View Javadoc
1    /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.commons.crypto;
19  
20  import java.io.BufferedInputStream;
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.StandardCopyOption;
27  import java.nio.file.attribute.PosixFileAttributes;
28  import java.util.Objects;
29  import java.util.Properties;
30  import java.util.UUID;
31  
32  import org.apache.commons.crypto.utils.Utils;
33  
34  /**
35   * A helper to load the native code i.e. libcommons-crypto.so. This handles the
36   * fallback to either the bundled libcommons-crypto-Linux-i386-32.so or the
37   * default java implementations where appropriate.
38   */
39  final class NativeCodeLoader {
40  
41      private static final String SIMPLE_NAME = NativeCodeLoader.class.getSimpleName();
42  
43      private static final String NATIVE_LIBNAME = "commons-crypto";
44  
45      private static final String NATIVE_LIBNAME_ALT = "lib" + NATIVE_LIBNAME + ".jnilib";
46  
47      /**
48       * End of file pseudo-character.
49       */
50      private static final int EOF = -1;
51  
52      private static final Throwable libraryLoadingError;
53  
54      private static final boolean libraryLoaded;
55  
56      static {
57          debug("%s static init start", SIMPLE_NAME);
58          libraryLoadingError = loadLibrary(); // will be null if loaded OK
59          libraryLoaded = libraryLoadingError == null;
60          debug("%s libraryLoaded = %s, libraryLoadingError = %s", SIMPLE_NAME, libraryLoaded, libraryLoadingError);
61          debug("%s static init end", SIMPLE_NAME);
62      }
63  
64      /**
65       * Returns the given InputStream if it is already a {@link BufferedInputStream},
66       * otherwise creates a BufferedInputStream from the given InputStream.
67       * <p>
68       * Copied from Apache Commons IO 2.5.
69       * </p>
70       *
71       * @param inputStream the InputStream to wrap or return (not null)
72       * @return the given InputStream or a new {@link BufferedInputStream} for the
73       *         given InputStream
74       * @throws NullPointerException if the input parameter is null
75       */
76      @SuppressWarnings("resource")
77      private static BufferedInputStream buffer(final InputStream inputStream) {
78          // reject null early on rather than waiting for IO operation to fail
79          // not checked by BufferedInputStream
80          Objects.requireNonNull(inputStream, "inputStream");
81          return inputStream instanceof BufferedInputStream ? (BufferedInputStream) inputStream
82                  : new BufferedInputStream(inputStream);
83      }
84  
85      /**
86       * Checks whether in1 and in2 is equal.
87       * <p>
88       * Copied from Apache Commons IO 2.5.
89       * </p>
90       *
91       * @param input1 the input1.
92       * @param input2 the input2.
93       * @return true if in1 and in2 is equal, else false.
94       * @throws IOException if an I/O error occurs.
95       */
96      @SuppressWarnings("resource")
97      private static boolean contentsEquals(final InputStream input1, final InputStream input2) throws IOException {
98          if (input1 == input2) {
99              return true;
100         }
101         if (input1 == null ^ input2 == null) {
102             return false;
103         }
104         final BufferedInputStream bufferedInput1 = buffer(input1);
105         final BufferedInputStream bufferedInput2 = buffer(input2);
106         int ch = bufferedInput1.read();
107         while (EOF != ch) {
108             final int ch2 = bufferedInput2.read();
109             if (ch != ch2) {
110                 return false;
111             }
112             ch = bufferedInput1.read();
113         }
114         return bufferedInput2.read() == EOF;
115     }
116 
117     /**
118      * Logs debug messages.
119      *
120      * @param format See {@link String#format(String, Object...)}.
121      * @param args   See {@link String#format(String, Object...)}.
122      */
123     private static void debug(final String format, final Object... args) {
124         // TODO Find a better way to do this later.
125         if (isDebug()) {
126             System.out.println(String.format(format, args));
127             if (args != null && args.length > 0 && args[0] instanceof Throwable) {
128                 ((Throwable) args[0]).printStackTrace(System.out);
129             }
130         }
131     }
132 
133     /**
134      * Extracts the specified library file to the target folder.
135      *
136      * @param libFolderForCurrentOS the library in commons-crypto.lib.path.
137      * @param libraryFileName       the library name.
138      * @param targetFolder          Target folder for the native lib. Use the value
139      *                              of commons-crypto.tempdir or java.io.tmpdir.
140      * @return the library file.
141      */
142     private static File extractLibraryFile(final String libFolderForCurrentOS, final String libraryFileName,
143             final String targetFolder) {
144         final String nativeLibraryFilePath = libFolderForCurrentOS + File.separator + libraryFileName;
145 
146         // Attach UUID to the native library file to ensure multiple class loaders
147         // can read the libcommons-crypto multiple times.
148         final UUID uuid = UUID.randomUUID();
149         final String extractedLibFileName = String.format("commons-crypto-%s-%s", uuid, libraryFileName);
150         final File extractedLibFile = new File(targetFolder, extractedLibFileName);
151         debug("Extracting '%s' to '%s'...", nativeLibraryFilePath, extractedLibFile);
152         try (InputStream inputStream = NativeCodeLoader.class.getResourceAsStream(nativeLibraryFilePath)) {
153             if (inputStream == null) {
154                 debug("Resource not found: %s", nativeLibraryFilePath);
155                 return null;
156             }
157             // Extract a native library file into the target directory
158             final Path path;
159             try {
160                 path = extractedLibFile.toPath();
161                 final long byteCount = Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING);
162                 if (isDebug()) {
163                     debug("Extracted '%s' to '%s': %,d bytes [%s]", nativeLibraryFilePath, extractedLibFile, byteCount,
164                             Files.isExecutable(path) ? "X+" : "X-");
165                     final PosixFileAttributes attributes = Files.readAttributes(path, PosixFileAttributes.class);
166                     if (attributes != null) {
167                         debug("Attributes '%s': %s %s %s", extractedLibFile, attributes.permissions(),
168                                 attributes.owner(), attributes.group());
169                     }
170                 }
171             } finally {
172                 // Delete the extracted lib file on JVM exit.
173                 debug("Delete on exit: %s", extractedLibFile);
174                 extractedLibFile.deleteOnExit();
175             }
176 
177             // Set executable (x) flag to enable Java to load the native library
178             if (!extractedLibFile.setReadable(true) || !extractedLibFile.setExecutable(true)
179                     || !extractedLibFile.setWritable(true, true)) {
180                 throw new IllegalStateException("Invalid path for library path " + extractedLibFile);
181             }
182 
183             // Check whether the contents are properly copied from the resource
184             // folder
185             try (InputStream nativeInputStream = NativeCodeLoader.class.getResourceAsStream(nativeLibraryFilePath)) {
186                 try (InputStream extractedLibIn = Files.newInputStream(path)) {
187                     debug("Validating '%s'...", extractedLibFile);
188                     if (!contentsEquals(nativeInputStream, extractedLibIn)) {
189                         throw new IllegalStateException(String.format("Failed to write a native library file %s to %s",
190                                 nativeLibraryFilePath, extractedLibFile));
191                     }
192                 }
193             }
194             return extractedLibFile;
195         } catch (final IOException e) {
196             debug("Ignoring %s", e);
197             return null;
198         }
199     }
200 
201     /**
202      * Finds the native library.
203      *
204      * @return the jar file.
205      */
206     private static File findNativeLibrary() {
207         // Get the properties once
208         final Properties props = Utils.getDefaultProperties();
209 
210         // Try to load the library in commons-crypto.lib.path */
211         String nativeLibraryPath = props.getProperty(Crypto.LIB_PATH_KEY);
212         String nativeLibraryName = props.getProperty(Crypto.LIB_NAME_KEY, System.mapLibraryName(NATIVE_LIBNAME));
213 
214         debug("%s nativeLibraryPath %s = %s", SIMPLE_NAME, Crypto.LIB_PATH_KEY, nativeLibraryPath);
215         debug("%s nativeLibraryName %s = %s", SIMPLE_NAME, Crypto.LIB_NAME_KEY, nativeLibraryName);
216 
217         if (nativeLibraryPath != null) {
218             final File nativeLib = new File(nativeLibraryPath, nativeLibraryName);
219             final boolean exists = nativeLib.exists();
220             debug("%s nativeLib %s exists = %s", SIMPLE_NAME, nativeLib, exists);
221             if (exists) {
222                 return nativeLib;
223             }
224         }
225 
226         // Load an OS-dependent native library inside a jar file
227         nativeLibraryPath = "/org/apache/commons/crypto/native/" + OsInfo.getNativeLibFolderPathForCurrentOS();
228         debug("%s nativeLibraryPath = %s", SIMPLE_NAME, nativeLibraryPath);
229         final String resource = nativeLibraryPath + File.separator + nativeLibraryName;
230         boolean hasNativeLib = hasResource(resource);
231         debug("%s resource %s exists = %s", SIMPLE_NAME, resource, hasNativeLib);
232         if (!hasNativeLib) {
233             final String altName = NATIVE_LIBNAME_ALT;
234             if (OsInfo.getOSName().equals("Mac") && hasResource(nativeLibraryPath + File.separator + altName)) {
235                 // Fix for openjdk7 for Mac
236                 nativeLibraryName = altName;
237                 hasNativeLib = true;
238             }
239         }
240 
241         if (!hasNativeLib) {
242             final String errorMessage = String.format("No native library is found for os.name=%s and os.arch=%s", OsInfo.getOSName(), OsInfo.getArchName());
243             throw new IllegalStateException(errorMessage);
244         }
245 
246         // Temporary folder for the native lib. Use the value of
247         // Crypto.LIB_TEMPDIR_KEY or java.io.tmpdir
248         final String tempFolder = new File(props.getProperty(Crypto.LIB_TEMPDIR_KEY, System.getProperty("java.io.tmpdir"))).getAbsolutePath();
249 
250         // Extract and load a native library inside the jar file
251         return extractLibraryFile(nativeLibraryPath, nativeLibraryName, tempFolder);
252     }
253 
254     /**
255      * Gets the error cause if loading failed.
256      *
257      * @return null, unless loading failed
258      */
259     static Throwable getLoadingError() {
260         return libraryLoadingError;
261     }
262 
263     /**
264      * Checks whether the given path has resource.
265      *
266      * @param path the path.
267      * @return the boolean.
268      */
269     private static boolean hasResource(final String path) {
270         return NativeCodeLoader.class.getResource(path) != null;
271     }
272 
273     private static boolean isDebug() {
274         return Boolean.getBoolean(Crypto.CONF_PREFIX + "debug");
275     }
276 
277     /**
278      * Checks whether native code is loaded for this platform.
279      *
280      * @return {@code true} if native is loaded, else {@code false}.
281      */
282     static boolean isNativeCodeLoaded() {
283         return libraryLoaded;
284     }
285 
286     /**
287      * Loads the library if possible.
288      *
289      * @return null if successful, otherwise the Throwable that was caught
290      */
291     static Throwable loadLibrary() {
292         try {
293             final File nativeLibFile = findNativeLibrary();
294             if (nativeLibFile != null) {
295                 // Load extracted or specified native library.
296                 final String absolutePath = nativeLibFile.getAbsolutePath();
297                 debug("%s System.load('%s')", SIMPLE_NAME, absolutePath);
298                 System.load(absolutePath);
299             } else {
300                 // Load preinstalled library (in the path -Djava.library.path)
301                 final String libname = NATIVE_LIBNAME;
302                 debug("%s System.loadLibrary('%s')", SIMPLE_NAME, libname);
303                 System.loadLibrary(libname);
304             }
305             return null; // OK
306         } catch (final Exception | UnsatisfiedLinkError t) {
307             return t;
308         }
309     }
310 
311     /**
312      * The private constructor of {@link NativeCodeLoader}.
313      */
314     private NativeCodeLoader() {
315     }
316 }