1 /*******************************************************************************
2  * Copyright (c) 2008, 2018 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 
15 /**
16  * The java.net.URLClassLoader class allows one to load resources from arbitrary URLs and in particular is optimized to handle
17  * "jar" URLs. Unfortunately for jar files this optimization ends up holding the file open which ultimately prevents the file from
18  * being deleted or update until the VM is shutdown.
19  *
20  * The CloseableURLClassLoader is meant to replace the URLClassLoader and provides an additional method to allow one to "close" any
21  * resources left open. In the current version the CloseableURLClassLoader will only ensure the closing of jar file resources. The
22  * jar handling behavior in this class will also provides a construct to allow one to turn off jar file verification in performance
23  * sensitive situations where the verification us not necessary.
24  *
25  * also see https://bugs.eclipse.org/bugs/show_bug.cgi?id=190279
26  */
27 
28 package org.eclipse.equinox.servletbridge;
29 
30 import java.io.*;
31 import java.lang.reflect.Method;
32 import java.net.*;
33 import java.security.*;
34 import java.util.*;
35 import java.util.jar.*;
36 import java.util.jar.Attributes.Name;
37 
38 public class CloseableURLClassLoader extends URLClassLoader {
39 	private static final boolean CLOSEABLE_REGISTERED_AS_PARALLEL;
40 	static {
41 		boolean registeredAsParallel;
42 		try {
43 			Method parallelCapableMetod = ClassLoader.class.getDeclaredMethod("registerAsParallelCapable", (Class[]) null); //$NON-NLS-1$
44 			parallelCapableMetod.setAccessible(true);
45 			registeredAsParallel = ((Boolean) parallelCapableMetod.invoke(null, (Object[]) null)).booleanValue();
46 		} catch (Throwable e) {
47 			// must do everything to avoid failing in clinit
48 			registeredAsParallel = true;
49 		}
50 		CLOSEABLE_REGISTERED_AS_PARALLEL = registeredAsParallel;
51 	}
52 	static final String DOT_CLASS = ".class"; //$NON-NLS-1$
53 	static final String BANG_SLASH = "!/"; //$NON-NLS-1$
54 	static final String JAR = "jar"; //$NON-NLS-1$
55 	private static final String UNC_PREFIX = "//"; //$NON-NLS-1$
56 	private static final String SCHEME_FILE = "file"; //$NON-NLS-1$
57 
58 	// @GuardedBy("loaders")
59 	final ArrayList<CloseableJarFileLoader> loaders = new ArrayList<>(); // package private to avoid synthetic access.
60 	// @GuardedBy("loaders")
61 	private final ArrayList<URL> loaderURLs = new ArrayList<>(); // note: protected by loaders
62 	// @GuardedBy("loaders")
63 	boolean closed = false; // note: protected by loaders, package private to avoid synthetic access.
64 
65 	private final AccessControlContext context;
66 	private final boolean verifyJars;
67 	private final boolean registeredAsParallel;
68 
69 	private static class CloseableJarURLConnection extends JarURLConnection {
70 		private final JarFile jarFile;
71 		// @GuardedBy("this")
72 		private JarEntry entry;
73 
CloseableJarURLConnection(URL url, JarFile jarFile)74 		public CloseableJarURLConnection(URL url, JarFile jarFile) throws MalformedURLException {
75 			super(url);
76 			this.jarFile = jarFile;
77 		}
78 
79 		@Override
connect()80 		public void connect() throws IOException {
81 			internalGetEntry();
82 		}
83 
internalGetEntry()84 		private synchronized JarEntry internalGetEntry() throws IOException {
85 			if (entry != null)
86 				return entry;
87 			entry = jarFile.getJarEntry(getEntryName());
88 			if (entry == null)
89 				throw new FileNotFoundException(getEntryName());
90 			return entry;
91 		}
92 
93 		@Override
getInputStream()94 		public InputStream getInputStream() throws IOException {
95 			return jarFile.getInputStream(internalGetEntry());
96 		}
97 
98 		/**
99 		 * @throws IOException
100 		 * Documented to avoid warning
101 		 */
102 		@Override
getJarFile()103 		public JarFile getJarFile() throws IOException {
104 			return jarFile;
105 		}
106 
107 		@Override
getJarEntry()108 		public JarEntry getJarEntry() throws IOException {
109 			return internalGetEntry();
110 		}
111 	}
112 
113 	private static class CloseableJarURLStreamHandler extends URLStreamHandler {
114 		private final JarFile jarFile;
115 
CloseableJarURLStreamHandler(JarFile jarFile)116 		public CloseableJarURLStreamHandler(JarFile jarFile) {
117 			this.jarFile = jarFile;
118 		}
119 
120 		@Override
openConnection(URL u)121 		protected URLConnection openConnection(URL u) throws IOException {
122 			return new CloseableJarURLConnection(u, jarFile);
123 		}
124 
125 		@Override
parseURL(URL u, String spec, int start, int limit)126 		protected void parseURL(URL u, String spec, int start, int limit) {
127 			setURL(u, JAR, null, 0, null, null, spec.substring(start, limit), null, null);
128 		}
129 	}
130 
131 	private static class CloseableJarFileLoader {
132 		private final JarFile jarFile;
133 		private final Manifest manifest;
134 		private final CloseableJarURLStreamHandler jarURLStreamHandler;
135 		private final String jarFileURLPrefixString;
136 
CloseableJarFileLoader(File file, boolean verify)137 		public CloseableJarFileLoader(File file, boolean verify) throws IOException {
138 			this.jarFile = new JarFile(file, verify);
139 			this.manifest = jarFile.getManifest();
140 			this.jarURLStreamHandler = new CloseableJarURLStreamHandler(jarFile);
141 			this.jarFileURLPrefixString = file.toURL().toString() + BANG_SLASH;
142 		}
143 
getURL(String name)144 		public URL getURL(String name) {
145 			if (jarFile.getEntry(name) != null)
146 				try {
147 					return new URL(JAR, null, -1, jarFileURLPrefixString + name, jarURLStreamHandler);
148 				} catch (MalformedURLException e) {
149 					// ignore
150 				}
151 			return null;
152 		}
153 
getManifest()154 		public Manifest getManifest() {
155 			return manifest;
156 		}
157 
close()158 		public void close() {
159 			try {
160 				jarFile.close();
161 			} catch (IOException e) {
162 				// ignore
163 			}
164 		}
165 	}
166 
167 	/**
168 	 * @param urls the array of URLs to use for loading resources
169 	 * @see URLClassLoader
170 	 */
CloseableURLClassLoader(URL[] urls)171 	public CloseableURLClassLoader(URL[] urls) {
172 		this(urls, ClassLoader.getSystemClassLoader(), true);
173 	}
174 
175 	/**
176 	 * @param urls the URLs from which to load classes and resources
177 	 * @param parent the parent class loader used for delegation
178 	 * @see URLClassLoader
179 	 */
CloseableURLClassLoader(URL[] urls, ClassLoader parent)180 	public CloseableURLClassLoader(URL[] urls, ClassLoader parent) {
181 		this(excludeFileJarURLS(urls), parent, true);
182 	}
183 
184 	/**
185 	 * @param urls the URLs from which to load classes and resources
186 	 * @param parent the parent class loader used for delegation
187 	 * @param verifyJars flag to determine if jar file verification should be performed
188 	 * @see URLClassLoader
189 	 */
CloseableURLClassLoader(URL[] urls, ClassLoader parent, boolean verifyJars)190 	public CloseableURLClassLoader(URL[] urls, ClassLoader parent, boolean verifyJars) {
191 		super(excludeFileJarURLS(urls), parent);
192 		this.registeredAsParallel = CLOSEABLE_REGISTERED_AS_PARALLEL && this.getClass() == CloseableURLClassLoader.class;
193 		this.context = AccessController.getContext();
194 		this.verifyJars = verifyJars;
195 		for (URL url : urls) {
196 			if (isFileJarURL(url)) {
197 				loaderURLs.add(url);
198 				safeAddLoader(url);
199 			}
200 		}
201 	}
202 
203 	// @GuardedBy("loaders")
safeAddLoader(URL url)204 	private boolean safeAddLoader(URL url) {
205 		//assume all illegal characters have been properly encoded, so use URI class to unencode
206 		try {
207 			File file = new File(toURI(url));
208 			if (file.exists()) {
209 				try {
210 					loaders.add(new CloseableJarFileLoader(file, verifyJars));
211 					return true;
212 				} catch (IOException e) {
213 					// ignore
214 				}
215 			}
216 		} catch (URISyntaxException e1) {
217 			// ignore
218 		}
219 
220 		return false;
221 	}
222 
toURI(URL url)223 	private static URI toURI(URL url) throws URISyntaxException {
224 		if (!SCHEME_FILE.equals(url.getProtocol())) {
225 			throw new IllegalArgumentException("bad prototcol: " + url.getProtocol()); //$NON-NLS-1$
226 		}
227 		//URL behaves differently across platforms so for file: URLs we parse from string form
228 		String pathString = url.toExternalForm().substring(5);
229 		//ensure there is a leading slash to handle common malformed URLs such as file:c:/tmp
230 		if (pathString.indexOf('/') != 0)
231 			pathString = '/' + pathString;
232 		else if (pathString.startsWith(UNC_PREFIX) && !pathString.startsWith(UNC_PREFIX, 2)) {
233 			//URL encodes UNC path with two slashes, but URI uses four (see bug 207103)
234 			pathString = ensureUNCPath(pathString);
235 		}
236 		return new URI(SCHEME_FILE, null, pathString, null);
237 	}
238 
239 	/**
240 	 * Ensures the given path string starts with exactly four leading slashes.
241 	 */
ensureUNCPath(String path)242 	private static String ensureUNCPath(String path) {
243 		int len = path.length();
244 		StringBuilder result = new StringBuilder(len);
245 		for (int i = 0; i < 4; i++) {
246 			//	if we have hit the first non-slash character, add another leading slash
247 			if (i >= len || result.length() > 0 || path.charAt(i) != '/')
248 				result.append('/');
249 		}
250 		result.append(path);
251 		return result.toString();
252 	}
253 
excludeFileJarURLS(URL[] urls)254 	private static URL[] excludeFileJarURLS(URL[] urls) {
255 		ArrayList<URL> urlList = new ArrayList<>();
256 		for (URL url : urls) {
257 			if (!isFileJarURL(url)) {
258 				urlList.add(url);
259 			}
260 		}
261 		return urlList.toArray(new URL[urlList.size()]);
262 	}
263 
isFileJarURL(URL url)264 	private static boolean isFileJarURL(URL url) {
265 		if (!url.getProtocol().equals("file")) //$NON-NLS-1$
266 			return false;
267 
268 		String path = url.getPath();
269 		if (path != null && path.endsWith("/")) //$NON-NLS-1$
270 			return false;
271 
272 		return true;
273 	}
274 
275 	@Override
findClass(final String name)276 	protected Class<?> findClass(final String name) throws ClassNotFoundException {
277 		try {
278 			Class<?> clazz = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
279 				@Override
280 				public Class<?> run() throws ClassNotFoundException {
281 					String resourcePath = name.replace('.', '/') + DOT_CLASS;
282 					CloseableJarFileLoader loader = null;
283 					URL resourceURL = null;
284 					synchronized (loaders) {
285 						if (closed)
286 							return null;
287 						for (Iterator<CloseableJarFileLoader> iterator = loaders.iterator(); iterator.hasNext();) {
288 							loader = iterator.next();
289 							resourceURL = loader.getURL(resourcePath);
290 							if (resourceURL != null)
291 								break;
292 						}
293 					}
294 					if (resourceURL != null) {
295 						try {
296 							return defineClass(name, resourceURL, loader.getManifest());
297 						} catch (IOException e) {
298 							throw new ClassNotFoundException(name, e);
299 						}
300 					}
301 					return null;
302 				}
303 			}, context);
304 			if (clazz != null)
305 				return clazz;
306 		} catch (PrivilegedActionException e) {
307 			throw (ClassNotFoundException) e.getException();
308 		}
309 		return super.findClass(name);
310 	}
311 
312 	// package private to avoid synthetic access.
defineClass(String name, URL resourceURL, Manifest manifest)313 	Class<?> defineClass(String name, URL resourceURL, Manifest manifest) throws IOException {
314 		JarURLConnection connection = (JarURLConnection) resourceURL.openConnection();
315 		int lastDot = name.lastIndexOf('.');
316 		if (lastDot != -1) {
317 			String packageName = name.substring(0, lastDot);
318 			synchronized (pkgLock) {
319 				Package pkg = getPackage(packageName);
320 				if (pkg != null) {
321 					checkForSealedPackage(pkg, packageName, manifest, connection.getJarFileURL());
322 				} else {
323 					definePackage(packageName, manifest, connection.getJarFileURL());
324 				}
325 			}
326 
327 		}
328 		JarEntry entry = connection.getJarEntry();
329 		byte[] bytes = new byte[(int) entry.getSize()];
330 		DataInputStream is = null;
331 		try {
332 			is = new DataInputStream(connection.getInputStream());
333 			is.readFully(bytes, 0, bytes.length);
334 			CodeSource cs = new CodeSource(connection.getJarFileURL(), entry.getCertificates());
335 			if (isRegisteredAsParallel()) {
336 				boolean initialLock = lockClassName(name);
337 				try {
338 					Class<?> clazz = findLoadedClass(name);
339 					if (clazz != null) {
340 						return clazz;
341 					}
342 					return defineClass(name, bytes, 0, bytes.length, cs);
343 				} finally {
344 					if (initialLock) {
345 						unlockClassName(name);
346 					}
347 				}
348 			}
349 			return defineClass(name, bytes, 0, bytes.length, cs);
350 		} finally {
351 			if (is != null)
352 				try {
353 					is.close();
354 				} catch (IOException e) {
355 					// ignore
356 				}
357 		}
358 	}
359 
checkForSealedPackage(Package pkg, String packageName, Manifest manifest, URL jarFileURL)360 	private void checkForSealedPackage(Package pkg, String packageName, Manifest manifest, URL jarFileURL) {
361 		if (pkg.isSealed()) {
362 			// previously sealed case
363 			if (!pkg.isSealed(jarFileURL)) {
364 				// this URL does not seal; ERROR
365 				throw new SecurityException("The package '" + packageName + "' was previously loaded and is already sealed."); //$NON-NLS-1$ //$NON-NLS-2$
366 			}
367 		} else {
368 			// previously unsealed case
369 			String entryPath = packageName.replace('.', '/') + "/"; //$NON-NLS-1$
370 			Attributes entryAttributes = manifest.getAttributes(entryPath);
371 			String sealed = null;
372 			if (entryAttributes != null)
373 				sealed = entryAttributes.getValue(Name.SEALED);
374 
375 			if (sealed == null) {
376 				Attributes mainAttributes = manifest.getMainAttributes();
377 				if (mainAttributes != null)
378 					sealed = mainAttributes.getValue(Name.SEALED);
379 			}
380 			if (Boolean.valueOf(sealed).booleanValue()) {
381 				// this manifest attempts to seal when package defined previously unsealed; ERROR
382 				throw new SecurityException("The package '" + packageName + "' was previously loaded unsealed. Cannot seal package."); //$NON-NLS-1$ //$NON-NLS-2$
383 			}
384 		}
385 	}
386 
387 	@Override
findResource(final String name)388 	public URL findResource(final String name) {
389 		URL url = AccessController.doPrivileged(new PrivilegedAction<URL>() {
390 			@Override
391 			public URL run() {
392 				synchronized (loaders) {
393 					if (closed)
394 						return null;
395 					for (CloseableJarFileLoader loader : loaders) {
396 						URL resourceURL = loader.getURL(name);
397 						if (resourceURL != null)
398 							return resourceURL;
399 					}
400 				}
401 				return null;
402 			}
403 		}, context);
404 		if (url != null)
405 			return url;
406 		return super.findResource(name);
407 	}
408 
409 	@Override
findResources(final String name)410 	public Enumeration<URL> findResources(final String name) throws IOException {
411 		final List<URL> resources = new ArrayList<>();
412 		AccessController.doPrivileged(new PrivilegedAction<Object>() {
413 			@Override
414 			public Object run() {
415 				synchronized (loaders) {
416 					if (closed)
417 						return null;
418 					for (CloseableJarFileLoader loader : loaders) {
419 						URL resourceURL = loader.getURL(name);
420 						if (resourceURL != null)
421 							resources.add(resourceURL);
422 					}
423 				}
424 				return null;
425 			}
426 		}, context);
427 		Enumeration<URL> e = super.findResources(name);
428 		while (e.hasMoreElements())
429 			resources.add(e.nextElement());
430 
431 		return Collections.enumeration(resources);
432 	}
433 
434 	/**
435 	 * The "close" method is called when the class loader is no longer needed and we should close any open resources.
436 	 * In particular this method will close the jar files associated with this class loader.
437 	 */
438 	@Override
close()439 	public void close() {
440 		synchronized (loaders) {
441 			if (closed)
442 				return;
443 			for (CloseableJarFileLoader loader : loaders) {
444 				loader.close();
445 			}
446 			closed = true;
447 		}
448 	}
449 
450 	@Override
addURL(URL url)451 	protected void addURL(URL url) {
452 		synchronized (loaders) {
453 			if (isFileJarURL(url)) {
454 				if (closed)
455 					throw new IllegalStateException("Cannot add url. CloseableURLClassLoader is closed."); //$NON-NLS-1$
456 				loaderURLs.add(url);
457 				if (safeAddLoader(url))
458 					return;
459 			}
460 		}
461 		super.addURL(url);
462 	}
463 
464 	@Override
getURLs()465 	public URL[] getURLs() {
466 		List<URL> result = new ArrayList<>();
467 		synchronized (loaders) {
468 			result.addAll(loaderURLs);
469 		}
470 		result.addAll(Arrays.asList(super.getURLs()));
471 		return result.toArray(new URL[result.size()]);
472 	}
473 
474 	private final Map<String, Thread> classNameLocks = new HashMap<>(5);
475 	private final Object pkgLock = new Object();
476 
lockClassName(String classname)477 	private boolean lockClassName(String classname) {
478 		synchronized (classNameLocks) {
479 			Object lockingThread = classNameLocks.get(classname);
480 			Thread current = Thread.currentThread();
481 			if (lockingThread == current)
482 				return false;
483 			boolean previousInterruption = Thread.interrupted();
484 			try {
485 				while (true) {
486 					if (lockingThread == null) {
487 						classNameLocks.put(classname, current);
488 						return true;
489 					}
490 
491 					classNameLocks.wait();
492 					lockingThread = classNameLocks.get(classname);
493 				}
494 			} catch (InterruptedException e) {
495 				current.interrupt();
496 				throw (LinkageError) new LinkageError(classname).initCause(e);
497 			} finally {
498 				if (previousInterruption) {
499 					current.interrupt();
500 				}
501 			}
502 		}
503 	}
504 
unlockClassName(String classname)505 	private void unlockClassName(String classname) {
506 		synchronized (classNameLocks) {
507 			classNameLocks.remove(classname);
508 			classNameLocks.notifyAll();
509 		}
510 	}
511 
isRegisteredAsParallel()512 	protected boolean isRegisteredAsParallel() {
513 		return registeredAsParallel;
514 	}
515 }
516