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