1/* 2 * Copyright 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17/* 18 * Copyright 2014 The Netty Project 19 * 20 * The Netty Project licenses this file to you under the Apache License, 21 * version 2.0 (the "License"); you may not use this file except in compliance 22 * with the License. You may obtain a copy of the License at: 23 * 24 * http://www.apache.org/licenses/LICENSE-2.0 25 * 26 * Unless required by applicable law or agreed to in writing, software 27 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 28 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 29 * License for the specific language governing permissions and limitations 30 * under the License. 31 */ 32package org.conscrypt; 33 34import java.io.ByteArrayOutputStream; 35import java.io.Closeable; 36import java.io.File; 37import java.io.FileOutputStream; 38import java.io.IOException; 39import java.io.InputStream; 40import java.io.OutputStream; 41import java.lang.reflect.Method; 42import java.net.URL; 43import java.security.AccessController; 44import java.security.PrivilegedAction; 45import java.util.Arrays; 46import java.util.Locale; 47 48/** 49 * Helper class to load JNI resources. 50 * 51 */ 52final class NativeLibraryLoader { 53 private static final String NATIVE_RESOURCE_HOME = "META-INF/native/"; 54 private static final String OSNAME; 55 private static final File WORKDIR; 56 57 static { 58 OSNAME = System.getProperty("os.name", "") 59 .toLowerCase(Locale.US) 60 .replaceAll("[^a-z0-9]+", ""); 61 62 String workdir = System.getProperty("org.conscrypt.native.workdir"); 63 if (workdir != null) { 64 File f = new File(workdir); 65 f.mkdirs(); 66 67 try { 68 f = f.getAbsoluteFile(); 69 } catch (Exception ignored) { 70 // Good to have an absolute path, but it's OK. 71 } 72 73 WORKDIR = f; 74 } else { 75 WORKDIR = tmpdir(); 76 } 77 } 78 79 private static File tmpdir() { 80 File f; 81 try { 82 f = toDirectory(System.getProperty("org.conscrypt.tmpdir")); 83 if (f != null) { 84 return f; 85 } 86 87 f = toDirectory(System.getProperty("java.io.tmpdir")); 88 if (f != null) { 89 return f; 90 } 91 92 // This shouldn't happen, but just in case .. 93 if (isWindows()) { 94 f = toDirectory(System.getenv("TEMP")); 95 if (f != null) { 96 return f; 97 } 98 99 String userprofile = System.getenv("USERPROFILE"); 100 if (userprofile != null) { 101 f = toDirectory(userprofile + "\\AppData\\Local\\Temp"); 102 if (f != null) { 103 return f; 104 } 105 106 f = toDirectory(userprofile + "\\Local Settings\\Temp"); 107 if (f != null) { 108 return f; 109 } 110 } 111 } else { 112 f = toDirectory(System.getenv("TMPDIR")); 113 if (f != null) { 114 return f; 115 } 116 } 117 } catch (Exception ignored) { 118 // Environment variable inaccessible 119 } 120 121 // Last resort. 122 if (isWindows()) { 123 f = new File("C:\\Windows\\Temp"); 124 } else { 125 f = new File("/tmp"); 126 } 127 128 return f; 129 } 130 131 @SuppressWarnings("ResultOfMethodCallIgnored") 132 private static File toDirectory(String path) { 133 if (path == null) { 134 return null; 135 } 136 137 File f = new File(path); 138 f.mkdirs(); 139 140 if (!f.isDirectory()) { 141 return null; 142 } 143 144 try { 145 return f.getAbsoluteFile(); 146 } catch (Exception ignored) { 147 return f; 148 } 149 } 150 151 private static boolean isWindows() { 152 return OSNAME.startsWith("windows"); 153 } 154 155 private static boolean isOSX() { 156 return OSNAME.startsWith("macosx") || OSNAME.startsWith("osx"); 157 } 158 159 /** 160 * Loads the first available library in the collection with the specified 161 * {@link ClassLoader}. 162 * 163 * @throws IllegalArgumentException 164 * if none of the given libraries load successfully. 165 */ 166 static void loadFirstAvailable(ClassLoader loader, String... names) { 167 for (String name : names) { 168 try { 169 load(name, loader); 170 return; 171 } catch (Throwable t) { 172 // Do nothing. 173 } 174 } 175 throw new IllegalArgumentException( 176 "Failed to load any of the given libraries: " + Arrays.toString(names)); 177 } 178 179 /** 180 * Load the given library with the specified {@link ClassLoader} 181 */ 182 private static void load(String name, ClassLoader loader) { 183 String libname = System.mapLibraryName(name); 184 String path = NATIVE_RESOURCE_HOME + libname; 185 186 URL url = loader.getResource(path); 187 if (url == null && isOSX()) { 188 if (path.endsWith(".jnilib")) { 189 url = loader.getResource(NATIVE_RESOURCE_HOME + "lib" + name + ".dynlib"); 190 } else { 191 url = loader.getResource(NATIVE_RESOURCE_HOME + "lib" + name + ".jnilib"); 192 } 193 } 194 195 if (url == null) { 196 // Fall back to normal loading of JNI stuff 197 loadLibrary(loader, name, false); 198 return; 199 } 200 201 int index = libname.lastIndexOf('.'); 202 String prefix = libname.substring(0, index); 203 String suffix = libname.substring(index, libname.length()); 204 InputStream in = null; 205 OutputStream out = null; 206 File tmpFile = null; 207 try { 208 tmpFile = createTempFile(prefix, suffix, WORKDIR); 209 in = url.openStream(); 210 out = new FileOutputStream(tmpFile); 211 212 byte[] buffer = new byte[8192]; 213 int length; 214 while ((length = in.read(buffer)) > 0) { 215 out.write(buffer, 0, length); 216 } 217 out.flush(); 218 219 // Close the output stream before loading the unpacked library, 220 // because otherwise Windows will refuse to load it when it's in use by other process. 221 closeQuietly(out); 222 out = null; 223 224 loadLibrary(loader, tmpFile.getPath(), true); 225 } catch (Exception e) { 226 throw(UnsatisfiedLinkError) new UnsatisfiedLinkError( 227 "could not load a native library: " + name) 228 .initCause(e); 229 } finally { 230 closeQuietly(in); 231 closeQuietly(out); 232 // After we load the library it is safe to delete the file. 233 // We delete the file immediately to free up resources as soon as possible, 234 // and if this fails fallback to deleting on JVM exit. 235 if (tmpFile != null && !tmpFile.delete()) { 236 tmpFile.deleteOnExit(); 237 } 238 } 239 } 240 241 /** 242 * Loading the native library into the specified {@link ClassLoader}. 243 * @param loader - The {@link ClassLoader} where the native library will be loaded into 244 * @param name - The native library path or name 245 * @param absolute - Whether the native library will be loaded by path or by name 246 */ 247 private static void loadLibrary( 248 final ClassLoader loader, final String name, final boolean absolute) { 249 try { 250 // Make sure the helper is belong to the target ClassLoader. 251 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class); 252 loadLibraryByHelper(newHelper, name, absolute); 253 return; 254 } catch (UnsatisfiedLinkError e) { // Should by pass the UnsatisfiedLinkError here! 255 // Do nothing. 256 } catch (Exception e) { 257 // Do nothing. 258 } 259 NativeLibraryUtil.loadLibrary(name, absolute); // Fallback to local helper class. 260 } 261 262 private static void loadLibraryByHelper(final Class<?> helper, final String name, 263 final boolean absolute) throws UnsatisfiedLinkError { 264 Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() { 265 @Override 266 public Object run() { 267 try { 268 // Invoke the helper to load the native library, if succeed, then the native 269 // library belong to the specified ClassLoader. 270 Method method = helper.getMethod("loadLibrary", String.class, boolean.class); 271 method.setAccessible(true); 272 return method.invoke(null, name, absolute); 273 } catch (Exception e) { 274 return e; 275 } 276 } 277 }); 278 if (ret instanceof Throwable) { 279 Throwable error = (Throwable) ret; 280 Throwable cause = error.getCause(); 281 if (cause != null) { 282 if (cause instanceof UnsatisfiedLinkError) { 283 throw(UnsatisfiedLinkError) cause; 284 } else { 285 throw new UnsatisfiedLinkError(cause.getMessage()); 286 } 287 } 288 throw new UnsatisfiedLinkError(error.getMessage()); 289 } 290 } 291 292 /** 293 * Try to load the helper {@link Class} into specified {@link ClassLoader}. 294 * @param loader - The {@link ClassLoader} where to load the helper {@link Class} 295 * @param helper - The helper {@link Class} 296 * @return A new helper Class defined in the specified ClassLoader. 297 * @throws ClassNotFoundException Helper class not found or loading failed 298 */ 299 private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper) 300 throws ClassNotFoundException { 301 try { 302 return loader.loadClass(helper.getName()); 303 } catch (ClassNotFoundException e) { 304 // The helper class is NOT found in target ClassLoader, we have to define the helper 305 // class. 306 final byte[] classBinary = classToByteArray(helper); 307 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() { 308 @Override 309 public Class<?> run() { 310 try { 311 // Define the helper class in the target ClassLoader, 312 // then we can call the helper to load the native library. 313 Method defineClass = ClassLoader.class.getDeclaredMethod( 314 "defineClass", String.class, byte[].class, int.class, int.class); 315 defineClass.setAccessible(true); 316 return (Class<?>) defineClass.invoke( 317 loader, helper.getName(), classBinary, 0, classBinary.length); 318 } catch (Exception e) { 319 throw new IllegalStateException("Define class failed!", e); 320 } 321 } 322 }); 323 } 324 } 325 326 /** 327 * Load the helper {@link Class} as a byte array, to be redefined in specified {@link 328 * ClassLoader}. 329 * @param clazz - The helper {@link Class} provided by this bundle 330 * @return The binary content of helper {@link Class}. 331 * @throws ClassNotFoundException Helper class not found or loading failed 332 */ 333 private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException { 334 String fileName = clazz.getName(); 335 int lastDot = fileName.lastIndexOf('.'); 336 if (lastDot > 0) { 337 fileName = fileName.substring(lastDot + 1); 338 } 339 URL classUrl = clazz.getResource(fileName + ".class"); 340 if (classUrl == null) { 341 throw new ClassNotFoundException(clazz.getName()); 342 } 343 byte[] buf = new byte[1024]; 344 ByteArrayOutputStream out = new ByteArrayOutputStream(4096); 345 InputStream in = null; 346 try { 347 in = classUrl.openStream(); 348 for (int r; (r = in.read(buf)) != -1;) { 349 out.write(buf, 0, r); 350 } 351 return out.toByteArray(); 352 } catch (IOException ex) { 353 throw new ClassNotFoundException(clazz.getName(), ex); 354 } finally { 355 closeQuietly(in); 356 closeQuietly(out); 357 } 358 } 359 360 private static void closeQuietly(Closeable c) { 361 if (c != null) { 362 try { 363 c.close(); 364 } catch (IOException ignore) { 365 // ignore 366 } 367 } 368 } 369 370 // Approximates the behavior of File.createTempFile without depending on SecureRandom. 371 private static File createTempFile(String prefix, String suffix, File directory) 372 throws IOException { 373 if (directory == null) { 374 throw new NullPointerException(); 375 } 376 long time = System.currentTimeMillis(); 377 prefix = new File(prefix).getName(); 378 IOException suppressed = null; 379 for (int i = 0; i < 10000; i++) { 380 String tempName = String.format("%s%d%04d%s", prefix, time, i, suffix); 381 File tempFile = new File(directory, tempName); 382 if (!tempName.equals(tempFile.getName())) { 383 // The given prefix or suffix contains path separators. 384 throw new IOException("Unable to create temporary file: " + tempFile); 385 } 386 try { 387 if (tempFile.createNewFile()) { 388 return tempFile.getCanonicalFile(); 389 } 390 } catch (IOException e) { 391 // This may just be a transient error; store it just in case. 392 suppressed = e; 393 } 394 } 395 if (suppressed != null) { 396 throw suppressed; 397 } else { 398 throw new IOException("Unable to create temporary file"); 399 } 400 } 401 402 private NativeLibraryLoader() { 403 // Utility 404 } 405} 406