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 to 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 to 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 to 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 to 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      * Appends the specified FileObjects to the list of FileObjects to search for classes and resources.
107      *
108      * @param manager The FileSystemManager.
109      * @param files the FileObjects to append to the search path.
110      * @throws FileSystemException if an error occurs.
111      */
112     private void addFileObjects(final FileSystemManager manager, final FileObject[] files) throws FileSystemException {
113         for (FileObject file : files) {
114             if (!FileObjectUtils.exists(file)) {
115                 // Does not exist - skip
116                 continue;
117             }
118 
119             // TODO - use federation instead
120             if (manager.canCreateFileSystem(file)) {
121                 // Use contents of the file
122                 file = manager.createFileSystem(file);
123             }
124 
125             resources.add(file);
126         }
127     }
128 
129     /**
130      * Copies the permissions from src to dest.
131      *
132      * @param src The source PermissionCollection.
133      * @param dest The destination PermissionCollection.
134      */
135     protected void copyPermissions(final PermissionCollection src, final PermissionCollection dest) {
136         for (final Enumeration<Permission> elem = src.elements(); elem.hasMoreElements();) {
137             dest.add(elem.nextElement());
138         }
139     }
140 
141     /**
142      * Loads and verifies the class with name and located with res.
143      */
144     private Class<?> defineClass(final String name, final Resource res) throws IOException {
145         final URL url = res.getCodeSourceURL();
146         final String pkgName = res.getPackageName();
147         if (pkgName != null) {
148             final Package pkg = getPackage(pkgName);
149             if (pkg != null) {
150                 if (pkg.isSealed()) {
151                     if (!pkg.isSealed(url)) {
152                         throw new FileSystemException("vfs.impl/pkg-sealed-other-url", pkgName);
153                     }
154                 } else if (isSealed(res)) {
155                     throw new FileSystemException("vfs.impl/pkg-sealing-unsealed", pkgName);
156                 }
157             } else {
158                 definePackage(pkgName, res);
159             }
160         }
161 
162         final byte[] bytes = res.getBytes();
163         final Certificate[] certs = res.getFileObject().getContent().getCertificates();
164         final CodeSource cs = new CodeSource(url, certs);
165         return defineClass(name, bytes, 0, bytes.length, cs);
166     }
167 
168     /**
169      * Reads attributes for the package and defines it.
170      */
171     private Package definePackage(final String name, final Resource res) throws FileSystemException {
172         // TODO - check for MANIFEST_ATTRIBUTES capability first
173         final String specTitle = res.getPackageAttribute(Name.SPECIFICATION_TITLE);
174         final String specVendor = res.getPackageAttribute(Attributes.Name.SPECIFICATION_VENDOR);
175         final String specVersion = res.getPackageAttribute(Name.SPECIFICATION_VERSION);
176         final String implTitle = res.getPackageAttribute(Name.IMPLEMENTATION_TITLE);
177         final String implVendor = res.getPackageAttribute(Name.IMPLEMENTATION_VENDOR);
178         final String implVersion = res.getPackageAttribute(Name.IMPLEMENTATION_VERSION);
179 
180         final URL sealBase;
181         if (isSealed(res)) {
182             sealBase = res.getCodeSourceURL();
183         } else {
184             sealBase = null;
185         }
186 
187         return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
188     }
189 
190     /**
191      * Finds and loads the class with the specified name from the search path.
192      *
193      * @throws ClassNotFoundException if the class is not found.
194      */
195     @Override
196     protected Class<?> findClass(final String name) throws ClassNotFoundException {
197         try {
198             final String path = name.replace('.', '/').concat(".class");
199             final Resource res = loadResource(path);
200             if (res == null) {
201                 throw new ClassNotFoundException(name);
202             }
203             return defineClass(name, res);
204         } catch (final IOException ioe) {
205             throw new ClassNotFoundException(name, ioe);
206         }
207     }
208 
209     /**
210      * Finds the resource with the specified name from the search path. This returns null if the resource is not found.
211      *
212      * @param name The resource name.
213      * @return The URL that matches the resource.
214      */
215     @Override
216     protected URL findResource(final String name) {
217         try {
218             final Resource res = loadResource(name);
219             if (res != null) {
220                 return res.getURL();
221             }
222             return null;
223         } catch (final Exception ignored) {
224             return null; // TODO: report?
225         }
226     }
227 
228     /**
229      * Returns an Enumeration of all the resources in the search path with the specified name.
230      * <p>
231      * Gets called from {@link ClassLoader#getResources(String)} after parent class loader was questioned.
232      *
233      * @param name The resources to find.
234      * @return An Enumeration of the resources associated with the name.
235      * @throws FileSystemException if an error occurs.
236      */
237     @Override
238     protected Enumeration<URL> findResources(final String name) throws IOException {
239         final List<URL> result = new ArrayList<>(2);
240 
241         for (final FileObject baseFile : resources) {
242             try (FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF)) {
243                 if (FileObjectUtils.exists(file)) {
244                     result.add(new Resource(name, baseFile, file).getURL());
245                 }
246             }
247         }
248 
249         return Collections.enumeration(result);
250     }
251 
252     /**
253      * Provide access to the file objects this class loader represents.
254      *
255      * @return An array of FileObjects.
256      * @since 2.0
257      */
258     public FileObject[] getFileObjects() {
259         return resources.toArray(FileObject.EMPTY_ARRAY);
260     }
261 
262     /**
263      * Calls super.getPermissions both for the code source and also adds the permissions granted to the parent layers.
264      *
265      * @param cs the CodeSource.
266      * @return The PermissionCollections.
267      */
268     @Override
269     protected PermissionCollection getPermissions(final CodeSource cs) {
270         try {
271             final String url = cs.getLocation().toString();
272             final FileObject file = lookupFileObject(url);
273             if (file == null) {
274                 return super.getPermissions(cs);
275             }
276 
277             final FileObject parentLayer = file.getFileSystem().getParentLayer();
278             if (parentLayer == null) {
279                 return super.getPermissions(cs);
280             }
281 
282             final Permissions combi = new Permissions();
283             PermissionCollection permCollect = super.getPermissions(cs);
284             copyPermissions(permCollect, combi);
285 
286             for (FileObject parent = parentLayer; parent != null; parent = parent.getFileSystem().getParentLayer()) {
287                 final CodeSource parentcs = new CodeSource(parent.getURL(), parent.getContent().getCertificates());
288                 permCollect = super.getPermissions(parentcs);
289                 copyPermissions(permCollect, combi);
290             }
291 
292             return combi;
293         } catch (final FileSystemException fse) {
294             throw new SecurityException(fse.getMessage());
295         }
296     }
297 
298     /**
299      * Returns true if we should seal the package where res resides.
300      */
301     private boolean isSealed(final Resource res) throws FileSystemException {
302         return Boolean.parseBoolean(res.getPackageAttribute(Attributes.Name.SEALED));
303     }
304 
305     /**
306      * Searches through the search path of for the first class or resource with specified name.
307      *
308      * @param name The resource to load.
309      * @return The Resource.
310      * @throws FileSystemException if an error occurs.
311      */
312     private Resource loadResource(final String name) throws FileSystemException {
313         for (final FileObject baseFile : resources) {
314             try (FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF)) {
315                 if (FileObjectUtils.exists(file)) {
316                     return new Resource(name, baseFile, file);
317                 }
318             }
319         }
320         return null;
321     }
322 
323     /**
324      * Does a reverse lookup to find the FileObject when we only have the URL.
325      */
326     private FileObject lookupFileObject(final String name) {
327         for (final FileObject object : resources) {
328             if (name.equals(object.getName().getURI())) {
329                 return object;
330             }
331         }
332         return null;
333     }
334 }