View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.impl;
18  
19  import java.io.IOException;
20  import java.net.URL;
21  import java.security.CodeSource;
22  import java.security.Permission;
23  import java.security.PermissionCollection;
24  import java.security.Permissions;
25  import java.security.SecureClassLoader;
26  import java.security.cert.Certificate;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.Enumeration;
30  import java.util.List;
31  import java.util.jar.Attributes;
32  import java.util.jar.Attributes.Name;
33  
34  import org.apache.commons.vfs2.FileObject;
35  import org.apache.commons.vfs2.FileSystemException;
36  import org.apache.commons.vfs2.FileSystemManager;
37  import org.apache.commons.vfs2.NameScope;
38  import org.apache.commons.vfs2.util.FileObjectUtils;
39  
40  /**
41   * A class loader that can load classes and resources from a search path.
42   * <p>
43   * The search path can consist of VFS FileObjects referring both to folders and JAR files. Any FileObject of type
44   * FileType.FILE is assumed to be a JAR and is opened by creating a layered file system with the "jar" scheme.
45   * </p>
46   * <p>
47   * TODO - Test this with signed Jars and a SecurityManager.
48   * </p>
49   *
50   * @see FileSystemManager#createFileSystem
51   */
52  public class VFSClassLoader extends SecureClassLoader {
53      private final ArrayList<FileObject> resources = new ArrayList<>();
54  
55      /**
56       * Constructors a new VFSClassLoader for the given file.
57       *
58       * @param file the file to load the classes and resources from.
59       * @param manager the FileManager to use when trying create a layered Jar file system.
60       * @throws FileSystemException if an error occurs.
61       */
62      public VFSClassLoader(final FileObject file, final FileSystemManager manager) throws FileSystemException {
63          this(new FileObject[] { file }, manager, null);
64      }
65  
66      /**
67       * Constructors a new VFSClassLoader for the given file.
68       *
69       * @param file the file to load the classes and resources from.
70       * @param manager the FileManager to use when trying create a layered Jar file system.
71       * @param parent the parent class loader for delegation.
72       * @throws FileSystemException if an error occurs.
73       */
74      public VFSClassLoader(final FileObject file, final FileSystemManager manager, final ClassLoader parent)
75              throws FileSystemException {
76          this(new FileObject[] { file }, manager, parent);
77      }
78  
79      /**
80       * Constructors a new VFSClassLoader for the given files. The files will be searched in the order specified.
81       *
82       * @param files the files to load the classes and resources from.
83       * @param manager the FileManager to use when trying create a layered Jar file system.
84       * @throws FileSystemException if an error occurs.
85       */
86      public VFSClassLoader(final FileObject[] files, final FileSystemManager manager) throws FileSystemException {
87          this(files, manager, null);
88      }
89  
90      /**
91       * Constructors a new VFSClassLoader for the given FileObjects. The FileObjects will be searched in the order
92       * specified.
93       *
94       * @param files the FileObjects to load the classes and resources from.
95       * @param manager the FileManager to use when trying create a layered Jar file system.
96       * @param parent the parent class loader for delegation.
97       * @throws FileSystemException if an error occurs.
98       */
99      public VFSClassLoader(final FileObject[] files, final FileSystemManager manager, final ClassLoader parent)
100             throws FileSystemException {
101         super(parent);
102         addFileObjects(manager, files);
103     }
104 
105     /**
106      * Provide access to the file objects this class loader represents.
107      *
108      * @return An array of FileObjects.
109      * @since 2.0
110      */
111     public FileObject[] getFileObjects() {
112         return resources.toArray(FileObject.EMPTY_ARRAY);
113     }
114 
115     /**
116      * Appends the specified FileObjects to the list of FileObjects to search for classes and resources.
117      *
118      * @param manager The FileSystemManager.
119      * @param files the FileObjects to append to the search path.
120      * @throws FileSystemException if an error occurs.
121      */
122     private void addFileObjects(final FileSystemManager manager, final FileObject[] files) throws FileSystemException {
123         for (FileObject file : files) {
124             if (!FileObjectUtils.exists(file)) {
125                 // Does not exist - skip
126                 continue;
127             }
128 
129             // TODO - use federation instead
130             if (manager.canCreateFileSystem(file)) {
131                 // Use contents of the file
132                 file = manager.createFileSystem(file);
133             }
134 
135             resources.add(file);
136         }
137     }
138 
139     /**
140      * Finds and loads the class with the specified name from the search path.
141      *
142      * @throws ClassNotFoundException if the class is not found.
143      */
144     @Override
145     protected Class<?> findClass(final String name) throws ClassNotFoundException {
146         try {
147             final String path = name.replace('.', '/').concat(".class");
148             final Resource res = loadResource(path);
149             if (res == null) {
150                 throw new ClassNotFoundException(name);
151             }
152             return defineClass(name, res);
153         } catch (final IOException ioe) {
154             throw new ClassNotFoundException(name, ioe);
155         }
156     }
157 
158     /**
159      * Loads and verifies the class with name and located with res.
160      */
161     private Class<?> defineClass(final String name, final Resource res) throws IOException {
162         final URL url = res.getCodeSourceURL();
163         final String pkgName = res.getPackageName();
164         if (pkgName != null) {
165             final Package pkg = getPackage(pkgName);
166             if (pkg != null) {
167                 if (pkg.isSealed()) {
168                     if (!pkg.isSealed(url)) {
169                         throw new FileSystemException("vfs.impl/pkg-sealed-other-url", pkgName);
170                     }
171                 } else if (isSealed(res)) {
172                     throw new FileSystemException("vfs.impl/pkg-sealing-unsealed", pkgName);
173                 }
174             } else {
175                 definePackage(pkgName, res);
176             }
177         }
178 
179         final byte[] bytes = res.getBytes();
180         final Certificate[] certs = res.getFileObject().getContent().getCertificates();
181         final CodeSource cs = new CodeSource(url, certs);
182         return defineClass(name, bytes, 0, bytes.length, cs);
183     }
184 
185     /**
186      * Returns true if the we should seal the package where res resides.
187      */
188     private boolean isSealed(final Resource res) throws FileSystemException {
189         final String sealed = res.getPackageAttribute(Attributes.Name.SEALED);
190         return "true".equalsIgnoreCase(sealed);
191     }
192 
193     /**
194      * Reads attributes for the package and defines it.
195      */
196     private Package definePackage(final String name, final Resource res) throws FileSystemException {
197         // TODO - check for MANIFEST_ATTRIBUTES capability first
198         final String specTitle = res.getPackageAttribute(Name.SPECIFICATION_TITLE);
199         final String specVendor = res.getPackageAttribute(Attributes.Name.SPECIFICATION_VENDOR);
200         final String specVersion = res.getPackageAttribute(Name.SPECIFICATION_VERSION);
201         final String implTitle = res.getPackageAttribute(Name.IMPLEMENTATION_TITLE);
202         final String implVendor = res.getPackageAttribute(Name.IMPLEMENTATION_VENDOR);
203         final String implVersion = res.getPackageAttribute(Name.IMPLEMENTATION_VERSION);
204 
205         final URL sealBase;
206         if (isSealed(res)) {
207             sealBase = res.getCodeSourceURL();
208         } else {
209             sealBase = null;
210         }
211 
212         return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
213     }
214 
215     /**
216      * Calls super.getPermissions both for the code source and also adds the permissions granted to the parent layers.
217      *
218      * @param cs the CodeSource.
219      * @return The PermissionCollections.
220      */
221     @Override
222     protected PermissionCollection getPermissions(final CodeSource cs) {
223         try {
224             final String url = cs.getLocation().toString();
225             final FileObject file = lookupFileObject(url);
226             if (file == null) {
227                 return super.getPermissions(cs);
228             }
229 
230             final FileObject parentLayer = file.getFileSystem().getParentLayer();
231             if (parentLayer == null) {
232                 return super.getPermissions(cs);
233             }
234 
235             final Permissions combi = new Permissions();
236             PermissionCollection permCollect = super.getPermissions(cs);
237             copyPermissions(permCollect, combi);
238 
239             for (FileObject parent = parentLayer; parent != null; parent = parent.getFileSystem().getParentLayer()) {
240                 final CodeSource parentcs = new CodeSource(parent.getURL(), parent.getContent().getCertificates());
241                 permCollect = super.getPermissions(parentcs);
242                 copyPermissions(permCollect, combi);
243             }
244 
245             return combi;
246         } catch (final FileSystemException fse) {
247             throw new SecurityException(fse.getMessage());
248         }
249     }
250 
251     /**
252      * Copies the permissions from src to dest.
253      *
254      * @param src The source PermissionCollection.
255      * @param dest The destination PermissionCollection.
256      */
257     protected void copyPermissions(final PermissionCollection src, final PermissionCollection dest) {
258         for (final Enumeration<Permission> elem = src.elements(); elem.hasMoreElements();) {
259             final Permission permission = elem.nextElement();
260             dest.add(permission);
261         }
262     }
263 
264     /**
265      * Does a reverse lookup to find the FileObject when we only have the URL.
266      */
267     private FileObject lookupFileObject(final String name) {
268         for (final FileObject object : resources) {
269             if (name.equals(object.getName().getURI())) {
270                 return object;
271             }
272         }
273         return null;
274     }
275 
276     /**
277      * Finds the resource with the specified name from the search path. This returns null if the resource is not found.
278      *
279      * @param name The resource name.
280      * @return The URL that matches the resource.
281      */
282     @Override
283     protected URL findResource(final String name) {
284         try {
285             final Resource res = loadResource(name);
286             if (res != null) {
287                 return res.getURL();
288             }
289             return null;
290         } catch (final Exception ignored) {
291             return null; // TODO: report?
292         }
293     }
294 
295     /**
296      * Returns an Enumeration of all the resources in the search path with the specified name.
297      * <p>
298      * Gets called from {@link ClassLoader#getResources(String)} after parent class loader was questioned.
299      *
300      * @param name The resources to find.
301      * @return An Enumeration of the resources associated with the name.
302      * @throws FileSystemException if an error occurs.
303      */
304     @Override
305     protected Enumeration<URL> findResources(final String name) throws IOException {
306         final List<URL> result = new ArrayList<>(2);
307 
308         for (final FileObject baseFile : resources) {
309             try (final FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF)) {
310                 if (FileObjectUtils.exists(file)) {
311                     result.add(new Resource(name, baseFile, file).getURL());
312                 }
313             }
314         }
315 
316         return Collections.enumeration(result);
317     }
318 
319     /**
320      * Searches through the search path of for the first class or resource with specified name.
321      *
322      * @param name The resource to load.
323      * @return The Resource.
324      * @throws FileSystemException if an error occurs.
325      */
326     private Resource loadResource(final String name) throws FileSystemException {
327         for (final FileObject baseFile : resources) {
328             try (final FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF)) {
329                 if (FileObjectUtils.exists(file)) {
330                     return new Resource(name, baseFile, file);
331                 }
332             }
333         }
334         return null;
335     }
336 }