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