All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.psr.vulas.java.JarWriter Maven / Gradle / Ivy

There is a newer version: 3.1.15
Show newest version
/**
 * This file is part of Eclipse Steady.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved.
 */
package com.sap.psr.vulas.java;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.sap.psr.vulas.core.util.CoreConfiguration;
import com.sap.psr.vulas.shared.util.DigestUtil;
import com.sap.psr.vulas.shared.util.DirUtil;
import com.sap.psr.vulas.shared.util.FileUtil;
import com.sap.psr.vulas.shared.util.VulasConfiguration;

/**
 * Utility class to extract and rewrite JAR files, offering the following possibilities to modify the JAR: Skip original manifest file entries, add new manifest file entries, add new JAR entries, replace the content of existing JAR entries (using JarEntryWriter).
 */
public class JarWriter {

	private static final Log log = LogFactory.getLog(JarWriter.class);

	public final SimpleDateFormat dateFormat = new SimpleDateFormat("d MMM yyyy HH:mm:ss");

	/**
	 * Included in the manifest file of every JAR rewritten by Vulas.
	 */
	public static final String MANIFEST_ENTRY_VULAS_MODIF = "VULAS-modifiedAt";

	/** Constant MANIFEST_ENTRY_ORIG_SHA1="VULAS-originalSHA1" */
	public static final String MANIFEST_ENTRY_ORIG_SHA1 = "VULAS-originalSHA1";

	/** Constant hexArray */
	final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();

	private JarFile originalJar = null;

	private long originalFileSize = 0;

	private Manifest originalManifest = null;

	private String sha1 = null;

	/** Original manifest entries to be skipped when rewriting the JAR. */
	private Set mfEntriesToSkip = new HashSet();

	/** New manifest entries to be added when rewriting the JAR. */
	private Map mfEntriesToAdd = new HashMap();

	/** Appended to the file name of the rewritten JAR. */
	private String classifier = null;

	/** JarEntryWriters to be called during rewrite, in case the entry name matches the provided pattern. */
	private Map callbacks = new HashMap();

	/** The rewritten JAR archive (null if rewrite has not yet been called). */
	private File rewrittenFile = null;

	/** Additional files to be written in the JAR (entryname:path). */
	private Map additionalFiles = new HashMap();

	/**
	 * 

Constructor for JarWriter.

* * @param _jar a {@link java.nio.file.Path} object. * @throws java.io.IOException if any. */ public JarWriter(Path _jar) throws IOException { final File file = _jar.toFile(); this.originalJar = new JarFile(file, VulasConfiguration.getGlobal().getConfiguration().getBoolean(CoreConfiguration.VERIFY_JARS, true), JarFile.OPEN_READ); this.originalFileSize = file.length(); this.originalManifest = this.originalJar.getManifest(); if(this.originalManifest==null) JarWriter.log.warn("Manifest file is missing in JAR [" + this.originalJar.getName() + "]"); } /** * Returns the size of the original JAR file. * * @see #getInstrumentedFileSize() * @return a long. */ public long getFileSize() { return this.originalFileSize; } /** * Returns the size of the instrumented JAR file or -1 if no instrumentation took place. * * @see #getFileSize() * @return a long. */ public long getInstrumentedFileSize() { if(this.rewrittenFile!=null) return this.rewrittenFile.length(); else return -1; } /** * Returns the original manifest. * * @return a {@link java.util.jar.Manifest} object. */ public Manifest getOriginalManifest() { return this.originalManifest; } /** * Extract the JAR to a given directory, or to a new temporary directory if null. * * @param _todir a {@link java.nio.file.Path} object. * @return a {@link java.nio.file.Path} object. * @throws java.io.IOException if any. */ public Path extract(Path _todir) throws IOException { // Target directory, to be returned Path to = _todir; if(to==null) to = java.nio.file.Files.createTempDirectory("extracted_jar_"); // Reading and writing the JAR entry to the FS Path path = null; File dir = null; byte[] bytes = new byte[1024]; int bytes_read = 0; // Loop all entries final Enumeration enumeration = this.originalJar.entries(); JarEntry entry = null; while(enumeration.hasMoreElements()) { entry = enumeration.nextElement(); // ZipSlip: Do not extract if(!DirUtil.isBelowDestinationPath(to, entry.getName())) { log.warn("Entry [" + entry + "] of archive [" + Paths.get(this.originalJar.getName()).toAbsolutePath() + "] will not be extracted, as it would be outside of destination directory"); } // Extract else { path = Paths.get(to.toString(), entry.getName()); try { if(entry.isDirectory()) { if(!path.toFile().exists()) Files.createDirectories(path); } else { // If the entry is a file, check whether we have already created the directory it is contained in. // According to the JAR spec [?], this should always be the case, but tests showed non-compliant JAR files. dir = path.getParent().toFile(); if(!dir.exists()) { Files.createDirectories(path.getParent()); JarWriter.log.warn(this.toString() + ": Invalid JAR file: No directory entry for file entry [" + path + "]"); } try (final FileOutputStream fos = new FileOutputStream(path.toFile()); final InputStream is = this.originalJar.getInputStream(entry)) { while((bytes_read = is.read(bytes)) != -1) fos.write(bytes, 0, bytes_read); } } } catch(Exception ioe) { JarWriter.log.error("Error while extracting JAR entry [" + entry.getName() + "]: " + ioe.getMessage(), ioe); } } } // Return the path to which the JAR was extracted JarWriter.log.info("Extracted [" + this.getOriginalJarFileName() + "] to [" + to + "]"); return to; } /** * Returns the SHA1 digest of the JAR. Either taken from the manifest (entry VULAS-originalSHA1, in case the original JAR has been instrumented * offline), or by computing it on the fly. * * @return the SHA1 digest of the JAR */ public synchronized String getSHA1() { if(this.sha1==null) { if(this.originalManifest!=null && this.originalManifest.getMainAttributes().getValue(MANIFEST_ENTRY_ORIG_SHA1)!=null) { this.sha1 = this.originalManifest.getMainAttributes().getValue(MANIFEST_ENTRY_ORIG_SHA1); } else { this.sha1 = FileUtil.getSHA1(new File(this.originalJar.getName())); } } return this.sha1; } /** * Entries of the original manifest which will not be rewritten. * Must be called before "rewrite". * * @param _entry a {@link java.lang.String} object. */ public void skipManifestEntry(String _entry) { this.mfEntriesToSkip.add(_entry); } /** * Additional manifest file entries to be included in re-written archives. * Must be called before "rewrite". * * @param _key a {@link java.lang.String} object. * @param _val a {@link java.lang.String} object. */ public void addManifestEntry(String _key, String _val) { this.mfEntriesToAdd.put(_key, _val); } /** * Returns true if the given manifest file entry exists, false otherwise. * * @param _key a {@link java.lang.String} object. * @return a boolean. */ public boolean hasManifestEntry(String _key) { //log.info(this.originalManifest.getMainAttributes().keySet()); if(this.originalManifest!=null) { for(Object key: this.originalManifest.getMainAttributes().keySet()) { if(_key.equals(key.toString())) return true; } } return false; // Does not work //return this.originalManifest.getMainAttributes().get(_key)!=null;//containsKey(_key); } /** * Returns true if the JAR has been rewritten by Vulas. Implemented by checking manifest file entries. * * @return a boolean. */ public boolean isRewrittenByVulas() { // Somehow the containsKey does not work, use getValue instead // final boolean modif = this.originalManifest.getMainAttributes().containsKey(JarWriter.MANIFEST_ENTRY_VULAS_MODIF); // final boolean sha1 = this.originalManifest.getMainAttributes().containsKey(JarWriter.MANIFEST_ENTRY_ORIG_SHA1); final boolean modif = this.originalManifest.getMainAttributes().getValue(JarWriter.MANIFEST_ENTRY_VULAS_MODIF)!=null; final boolean sha1 = this.originalManifest.getMainAttributes().getValue(JarWriter.MANIFEST_ENTRY_ORIG_SHA1)!=null; return modif && sha1; } /** * Will be appended to the file name of re-written archives (if any). * Must be called before "rewrite". * * @param _string a {@link java.lang.String} object. */ public void setClassifier(String _string) { this.classifier = _string; } /** * Register a JarEntryWriter for a given pattern. * * @param _regex a {@link java.lang.String} object. * @param _writer a {@link com.sap.psr.vulas.java.JarEntryWriter} object. */ public void register(String _regex, JarEntryWriter _writer) { this.callbacks.put(Pattern.compile(_regex), _writer); } /** * @return */ private Manifest createModifiedManifest() { final Manifest m = new Manifest(); final Attributes atts = m.getMainAttributes(); // Put all the main attributes of the original JAR if(this.originalManifest!=null) { for(Object key: this.originalManifest.getMainAttributes().keySet()) { // Unless it is one to be skipped if(!this.mfEntriesToSkip.contains(key.toString())) atts.putValue(key.toString(), this.originalManifest.getMainAttributes().getValue(key.toString())); } } // Put all the new entries for(String key: this.mfEntriesToAdd.keySet()) { atts.putValue(key, this.mfEntriesToAdd.get(key)); } // Add vulas-specific ones atts.putValue(JarWriter.MANIFEST_ENTRY_VULAS_MODIF, dateFormat.format(new Date(System.currentTimeMillis()))); atts.putValue(JarWriter.MANIFEST_ENTRY_ORIG_SHA1, this.getSHA1()); return m; } /** * Returns the file name of the original JAR file. * * @return a {@link java.nio.file.Path} object. */ public Path getOriginalJarFileName() { final Path complete_path = Paths.get(this.originalJar.getName()); return complete_path.getFileName(); } /** * Returns the file name of the to-be-rewritten JAR. * * @return a {@link java.nio.file.Path} object. */ public Path getRewriteJarFileName() { final Path complete_path = Paths.get(this.originalJar.getName()); Path path = null; // Classifier exists, i.e., change file name if(this.classifier!=null) { final String original_filename = this.getOriginalJarFileName().toString(); String new_filename = null; final int idx = original_filename.lastIndexOf('.'); if(idx!=-1) new_filename = original_filename.substring(0, idx) + "-" + this.classifier + original_filename.substring(idx); else new_filename = original_filename + "-" + this.classifier; path = Paths.get(new_filename); } // Use original file name else { path = complete_path.getFileName(); } return path; } /** *

addFiles.

* * @param _target_dir a {@link java.lang.String} object. * @param _paths a {@link java.util.Set} object. * @param _overwrite a boolean. */ public void addFiles(String _target_dir, Set _paths, boolean _overwrite) { for(Path p: _paths) this.addFile(_target_dir, p, _overwrite); } /** *

addFile.

* * @param _target_dir a {@link java.lang.String} object. * @param _path a {@link java.nio.file.Path} object. * @param _overwrite a boolean. */ public void addFile(String _target_dir, Path _path, boolean _overwrite) { String entry_name = null; if(_target_dir==null) entry_name = _path.getFileName().toString(); else entry_name = _target_dir + (_target_dir.equals("") || _target_dir.endsWith("/") ? "" : "/") + _path.getFileName(); if(!this.hasEntry(entry_name) || _overwrite) this.additionalFiles.put(entry_name, _path); } /** *

hasEntry.

* * @param _entry_name a {@link java.lang.String} object. * @return a boolean. */ public boolean hasEntry(String _entry_name) { boolean has = false; final Enumeration en = this.originalJar.entries(); JarEntry entry = null; while(en.hasMoreElements()) { entry = en.nextElement(); if(entry.getName().equals(_entry_name)) { has = true; break; } } return has; } /** * The rewritten JAR file. Must be called after "rewrite". If rewrite is called multiple times, this method * only returns the last rewritten JAR file. * * @return a {@link java.io.File} object. */ public File getRewrittenJarFile() { return this.rewrittenFile; } /** * Rewrites the JAR into the directory specified by argument _todir. If _todir is equal to null, * the JAR will be rewritten to a temporary directory. If the target JAR already exists, it will * not be written (see {@link JarWriter#getRewriteJarFileName()}). * See here: http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html * * @throws com.sap.psr.vulas.java.JarAnalysisException * @param _todir a {@link java.nio.file.Path} object. * @return a {@link java.nio.file.Path} object. */ public Path rewrite(Path _todir) throws JarAnalysisException { // Target dir Path dir = _todir; // Callback logic Matcher matcher = null; // Loop all entries of the old JAR JarEntry old_entry = null, new_entry = null; try { if(dir==null) dir = java.nio.file.Files.createTempDirectory("rewritten_jar_"); this.rewrittenFile = Paths.get(dir.toString(), this.getRewriteJarFileName().toString()).toFile(); if(this.rewrittenFile.exists()) { JarWriter.log.info("The target [" + this.rewrittenFile + "] already exists, skip rewriting"); } else { final FileOutputStream fos = new FileOutputStream(this.rewrittenFile); final JarOutputStream jos = new JarOutputStream(fos, this.createModifiedManifest()); InputStream is = null; byte[] bytes = new byte[1024]; int bytes_read = 0; final Enumeration en = this.originalJar.entries(); String class_name = null; JavaId jid = null; Set jids = new HashSet(); // Remember all JAR entries written to the new JAR, so that we do not create duplicate entries // Example of a duplicate entry: Location.class in xmlbeans-2.6.0.jar Set written_jar_entries = new HashSet(); while(en.hasMoreElements()) { old_entry = en.nextElement(); // The input stream used for writing the entries is = null; // Check whether we already write an entry with this name if(written_jar_entries.contains(old_entry.getName())) continue; // Ignore the original manifest (we built a new one with certain vulas-specific attributes) if(old_entry.getName().equals("META-INF/MANIFEST.MF")) continue; // Ignore signature related files (-> http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html#Signed_JAR_File) if(old_entry.getName().startsWith("META-INF/") && ( old_entry.getName().toLowerCase().endsWith(".sf") || old_entry.getName().toLowerCase().endsWith(".dsa") || old_entry.getName().toLowerCase().endsWith(".rsa") ) ) continue; // Loop registered JarEntryWriters to see if any matches (take the input stream from the first match) for(Pattern pattern: this.callbacks.keySet()) { matcher = pattern.matcher(old_entry.getName()); if(matcher.matches()) { is = this.callbacks.get(pattern).getInputStream(pattern.toString(), old_entry); } } // If null, take the original file if(is==null) is = this.originalJar.getInputStream(old_entry); // Debug information regarding specific attributes if(old_entry.getAttributes()!=null) JarWriter.log.debug(this.toString() + ": Entry [" + old_entry.getName() + "] has specific attributes"); // Write the entry to the modified JAR new_entry = new JarEntry(old_entry.getName()); jos.putNextEntry(new_entry); while((bytes_read = is.read(bytes)) != -1) jos.write(bytes, 0, bytes_read); //is.close(); jos.closeEntry(); // Remember we wrote it written_jar_entries.add(new_entry.getName()); } // Add additional files for(String key: this.additionalFiles.keySet()) { if(this.additionalFiles.get(key).toFile().exists()) { new_entry = new JarEntry(key); jos.putNextEntry(new_entry); is = new FileInputStream(this.additionalFiles.get(key).toFile()); while((bytes_read = is.read(bytes)) != -1) jos.write(bytes, 0, bytes_read); is.close(); jos.closeEntry(); } } jos.flush(); jos.close(); this.originalJar.close(); // old_entry = null; JarWriter.log.info("[" + this.originalJar.getName() + "] rewritten to [" + this.rewrittenFile + "]"); } } catch(Exception ioe) { if(old_entry!=null) throw new JarAnalysisException("Error while writing JAR entry [" + old_entry.getName() + "] to modified JAR [" + this.rewrittenFile + "]: " + ioe.getMessage(), ioe); else throw new JarAnalysisException("Error while writing modified JAR: " + ioe.getMessage(), ioe); } return this.rewrittenFile.toPath(); } /** *

appendToClasspath.

* * @param _classpath a {@link java.util.Set} object. * @param _to_append a {@link java.util.Set} object. * @param _preprocess a boolean. */ public final static void appendToClasspath(Set _classpath, Set _to_append, boolean _preprocess) { for(Path p: _to_append) appendToClasspath(_classpath, p, _preprocess); } /** * Appends the given {@link Path} to the given set. In case of Java archives, it is checked whether it contains * a manifest entry "Class-Path", in which case the archive is re-written to a temporary file w/o this entry. * The method returns the path that has been appended, which is identical to the given path unless an archive * has been rewritten. * * TODO: Maybe add a parameter to specify problematic entries, rather than hardcoding "Class-Path" here. * * @param _classpath a {@link java.util.Set} object. * @param _to_append a {@link java.nio.file.Path} object. * @param _preprocess a boolean. * @return a {@link java.nio.file.Path} object. */ public final static Path appendToClasspath(Set _classpath, Path _to_append, boolean _preprocess) { Path appended_path = _to_append; // Add w/o preprocessing if(!_preprocess || _to_append.toFile().isDirectory()) { _classpath.add(_to_append); } // Add after preprocessing (if needed) else { try { // Create a new Jar Writer final JarWriter jw = new JarWriter(_to_append); // Class-Path manifest entry is present: Rewrite JAR and append if(jw.hasManifestEntry("Class-Path")) { jw.skipManifestEntry("Class-Path"); jw.setClassifier(jw.getSHA1()); appended_path = jw.rewrite(VulasConfiguration.getGlobal().getTmpDir()); _classpath.add(appended_path); } // Entry not present: Just add to classpath else { JarWriter.log.info("Rewriting not necessary, original JAR [" + _to_append + "] appended to classpath"); _classpath.add(_to_append); } } // Add original JAR in case of exception catch (Exception e) { _classpath.add(_to_append); JarWriter.log.error("Error while preprocessing JAR [" + _to_append + "], original JAR appended to classpath: " + e.getMessage()); } } return appended_path; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy