001/*
002 * Copyright (C) 2014 Jörg Prante
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.xbib.elasticsearch.plugin.jdbc.classloader.uri;
017
018import org.xbib.elasticsearch.plugin.jdbc.classloader.ResourceHandle;
019
020import java.io.File;
021import java.io.IOException;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.net.URL;
025import java.net.URLClassLoader;
026import java.security.AccessControlContext;
027import java.security.AccessController;
028import java.security.CodeSource;
029import java.security.PrivilegedAction;
030import java.security.PrivilegedActionException;
031import java.security.PrivilegedExceptionAction;
032import java.security.cert.Certificate;
033import java.util.Enumeration;
034import java.util.jar.Attributes;
035import java.util.jar.Attributes.Name;
036import java.util.jar.Manifest;
037
038/**
039 * Equivalent of java.net.URLClassloader but without bugs related to ill-formed
040 * URLs and with customizable JAR caching policy. The standard URLClassLoader
041 * accepts URLs containing spaces and other characters which are forbidden in
042 * the URI syntax, according to the RFC 2396. As a workaround to this problem,
043 * Java escapes and un-escapes URLs in various arbitrary places; however, this
044 * is inconsistent and leads to numerous problems with URLs referring to local
045 * files with spaces in the path. SUN acknowledges the problem, but refuses to
046 * modify the behavior for compatibility reasons; see Java Bug Parade 4273532,
047 * 4466485.
048 * <p/>
049 * Additionally, the JAR caching policy used by URLClassLoader is system-wide
050 * and inflexible: once downloaded JAR files are never re-downloaded, even if
051 * one creates a fresh instance of the class loader that happens to have the
052 * same URL in its search path. In fact, that policy is a security
053 * vulnerability: it is possible to crash any URL class loader, thus affecting
054 * potentially separate part of the system, by creating URL connection to one of
055 * the URLs of that class loader search path and closing the associated JAR
056 * file. See Java Bug Parade 4405789, 4388666, 4639900.
057 * <p/>
058 * This class avoids these problems by 1) using URIs instead of URLs for the
059 * search path (thus enforcing strict syntax conformance and defining precise
060 * escaping semantics), and 2) using custom URLStreamHandler which ensures
061 * per-classloader JAR caching policy.
062 */
063public final class URIClassLoader extends URLClassLoader {
064
065    private final URIResourceFinder finder = new URIResourceFinder();
066
067    private final AccessControlContext acc;
068
069    /**
070     * Creates URIClassLoader
071     */
072    public URIClassLoader() {
073        this(null);
074    }
075
076    /**
077     * Creates URIClassLoader with the specified parent class loader.
078     *
079     * @param parent the parent class loader.
080     */
081    public URIClassLoader(ClassLoader parent) {
082        super(new URL[0], parent);
083        this.acc = AccessController.getContext();
084    }
085
086    /**
087     * Add specified URI at the end of the search path.
088     *
089     * @param uri the URI to add
090     */
091    public URIClassLoader addURI(URI uri) {
092        finder.addURI(uri);
093        return this;
094    }
095
096    public URI[] getURIs() {
097        return finder.getURIs();
098    }
099
100    /**
101     * Add specified URL at the end of the search path.
102     *
103     * @param url the URL to add
104     * @deprecated use addURI
105     */
106    @Override
107    @Deprecated
108    protected void addURL(URL url) {
109        try {
110            finder.addURI(url.toURI());
111        } catch (URISyntaxException e) {
112            // ignore
113        }
114    }
115
116    /**
117     * Finds and loads the class with the specified name.
118     *
119     * @param name the name of the class
120     * @return the resulting class
121     * @throws ClassNotFoundException if the class could not be found
122     */
123    @Override
124    protected Class findClass(final String name) throws ClassNotFoundException {
125        try {
126            return (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
127                @Override
128                public Object run() throws ClassNotFoundException {
129                    String path = name.replace('.', '/').concat(".class");
130                    ResourceHandle h = finder.getResource(path);
131                    if (h != null) {
132                        try {
133                            return defineClass(name, h);
134                        } catch (IOException e) {
135                            throw new ClassNotFoundException(name, e);
136                        }
137                    } else {
138                        throw new ClassNotFoundException(name);
139                    }
140                }
141            }, acc);
142        } catch (PrivilegedActionException pae) {
143            throw (ClassNotFoundException) pae.getException();
144        }
145    }
146
147    /**
148     * Finds the resource with the specified name.
149     *
150     * @param name the name of the resource
151     * @return a <code>URL</code> for the resource, or <code>null</code> if the
152     * resource could not be found.
153     */
154    @Override
155    public URL findResource(final String name) {
156        return (URL) AccessController.doPrivileged(new PrivilegedAction() {
157            public Object run() {
158                return finder.findResource(name);
159            }
160        }, acc);
161    }
162
163    /**
164     * Returns an Enumeration of URLs representing all of the resources having
165     * the specified name.
166     *
167     * @param name the resource name
168     * @return an <code>Enumeration</code> of <code>URL</code>s
169     * @throws java.io.IOException if an I/O exception occurs
170     */
171    @Override
172    public Enumeration<URL> findResources(final String name) throws IOException {
173        return (Enumeration<URL>) AccessController.doPrivileged(new PrivilegedAction() {
174            public Object run() {
175                return finder.findResources(name);
176            }
177        }, acc);
178    }
179
180    /**
181     * Returns the absolute path name of a native library. The VM invokes this
182     * method to locate the native libraries that belong to classes loaded with
183     * this class loader. If this method returns
184     * <code>null</code>, the VM searches the library along the path specified
185     * as the
186     * <code>java.library.path</code> property. This method invoke
187     * {@link #getLibraryHandle} method to find handle of this library. If the
188     * handle is found and its URL protocol is "file", the system-dependent
189     * absolute library file path is returned. Otherwise this method returns
190     * null. <p>
191     * <p/>
192     * Subclasses can override this method to provide specific approaches in
193     * library searching.
194     *
195     * @param libname the library name
196     * @return the absolute path of the native library
197     * @see System#loadLibrary(String)
198     * @see System#mapLibraryName(String)
199     */
200    @Override
201    protected String findLibrary(String libname) {
202        ResourceHandle md = getLibraryHandle(libname);
203        if (md == null) {
204            return null;
205        }
206        URL url = md.getUrl();
207        if (!"file".equals(url.getProtocol())) {
208            return null;
209        }
210        return new File(URI.create(url.toString())).getPath();
211    }
212
213    /**
214     * Finds the ResourceHandle object for the resource with the specified name.
215     *
216     * @param name the name of the resource
217     * @return the ResourceHandle of the resource
218     */
219    private ResourceHandle getResourceHandle(final String name) {
220        return (ResourceHandle) AccessController.doPrivileged(new PrivilegedAction() {
221            public Object run() {
222                return finder.getResource(name);
223            }
224        }, acc);
225    }
226
227    /**
228     * Finds the ResourceHandle object for the native library with the specified
229     * name. The library name must be '/'-separated path. The last part of this
230     * path is substituted by its system-dependent mapping (using
231     * {@link System#mapLibraryName(String)} method). Next, the
232     * <code>ResourceFinder</code> is used to look for the library as it was
233     * ordinary resource. <p>
234     * <p/>
235     * Subclasses can override this method to provide specific approaches in
236     * library searching.
237     *
238     * @param name the name of the library
239     * @return the ResourceHandle of the library
240     */
241    private ResourceHandle getLibraryHandle(final String name) {
242        int idx = name.lastIndexOf('/');
243        String path;
244        String simplename;
245        if (idx == -1) {
246            path = "";
247            simplename = name;
248        } else if (idx == name.length() - 1) { // name.endsWith('/')
249            throw new IllegalArgumentException(name);
250        } else {
251            path = name.substring(0, idx + 1); // including '/'
252            simplename = name.substring(idx + 1);
253        }
254        return getResourceHandle(path + System.mapLibraryName(simplename));
255    }
256
257    /**
258     * Returns true if the specified package name is sealed according to the
259     * given manifest.
260     */
261    private boolean isSealed(String name, Manifest man) {
262        String path = name.replace('.', '/').concat("/");
263        Attributes attr = man.getAttributes(path);
264        String sealed = null;
265        if (attr != null) {
266            sealed = attr.getValue(Name.SEALED);
267        }
268        if (sealed == null) {
269            if ((attr = man.getMainAttributes()) != null) {
270                sealed = attr.getValue(Name.SEALED);
271            }
272        }
273        return "true".equalsIgnoreCase(sealed);
274    }
275
276    private Class defineClass(String name, ResourceHandle h) throws IOException {
277        int i = name.lastIndexOf('.');
278        URL url = h.getCodeSourceUrl();
279        if (i != -1) { // check package
280            String pkgname = name.substring(0, i);
281            // check if package already loaded
282            Package pkg = getPackage(pkgname);
283            Manifest man = h.getManifest();
284            if (pkg != null) {
285                // package found, so check package sealing
286                boolean ok;
287                if (pkg.isSealed()) {
288                    // verify that code source URLs are the same
289                    ok = pkg.isSealed(url);
290                } else {
291                    // make sure we are not attempting to seal the package
292                    // at this code source URL
293                    ok = (man == null) || !isSealed(pkgname, man);
294                }
295                if (!ok) {
296                    throw new SecurityException("sealing violation: " + name);
297                }
298            } else { // package not yet defined
299                if (man != null) {
300                    definePackage(pkgname, man, url);
301                } else {
302                    definePackage(pkgname, null, null, null, null, null, null, null);
303                }
304            }
305        }
306        // now read the class bytes and define the class
307        byte[] b = h.getBytes();
308        Certificate[] certs = h.getCertificates();
309        CodeSource cs = new CodeSource(url, certs);
310        return defineClass(name, b, 0, b.length, cs);
311    }
312
313    public String toString() {
314        StringBuilder sb = new StringBuilder();
315        sb.append("ClassLoader:");
316        for (URI uri : getURIs()) {
317            sb.append("[").append(uri).append("]");
318        }
319        return sb.toString();
320    }
321}