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