| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| package org.apache.maven.plugins.jarsigner; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.ResourceBundle; |
| |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.execution.MavenSession; |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugins.annotations.Parameter; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.settings.Settings; |
| import org.apache.maven.shared.jarsigner.JarSigner; |
| import org.apache.maven.shared.jarsigner.JarSignerRequest; |
| import org.apache.maven.shared.jarsigner.JarSignerUtil; |
| import org.apache.maven.shared.utils.ReaderFactory; |
| import org.apache.maven.shared.utils.StringUtils; |
| import org.apache.maven.shared.utils.cli.Commandline; |
| import org.apache.maven.shared.utils.cli.javatool.JavaToolException; |
| import org.apache.maven.shared.utils.io.FileUtils; |
| import org.apache.maven.toolchain.Toolchain; |
| import org.apache.maven.toolchain.ToolchainManager; |
| import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher; |
| import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException; |
| |
| /** |
| * Maven Jarsigner Plugin base class. |
| * |
| * @author <a href="cs@schulte.it">Christian Schulte</a> |
| */ |
| public abstract class AbstractJarsignerMojo extends AbstractMojo { |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter(property = "jarsigner.verbose", defaultValue = "false") |
| private boolean verbose; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter(property = "jarsigner.keystore") |
| private String keystore; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter(property = "jarsigner.storetype") |
| private String storetype; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter(property = "jarsigner.storepass") |
| private String storepass; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter(property = "jarsigner.providerName") |
| private String providerName; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter(property = "jarsigner.providerClass") |
| private String providerClass; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter(property = "jarsigner.providerArg") |
| private String providerArg; |
| |
| /** |
| * See <a href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options">options</a>. |
| */ |
| @Parameter(property = "jarsigner.alias") |
| private String alias; |
| |
| /** |
| * The maximum memory available to the JAR signer, e.g. <code>256M</code>. See <a |
| * href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/java.html#Xms">-Xmx</a> for more details. |
| */ |
| @Parameter(property = "jarsigner.maxMemory") |
| private String maxMemory; |
| |
| /** |
| * Archive to process. If set, neither the project artifact nor any attachments or archive sets are processed. |
| */ |
| @Parameter(property = "jarsigner.archive") |
| private File archive; |
| |
| /** |
| * The base directory to scan for JAR files using Ant-like inclusion/exclusion patterns. |
| * |
| * @since 1.1 |
| */ |
| @Parameter(property = "jarsigner.archiveDirectory") |
| private File archiveDirectory; |
| |
| /** |
| * The Ant-like inclusion patterns used to select JAR files to process. The patterns must be relative to the |
| * directory given by the parameter {@link #archiveDirectory}. By default, the pattern |
| * <code>**/*.?ar</code> is used. |
| * |
| * @since 1.1 |
| */ |
| @Parameter |
| private String[] includes = {"**/*.?ar"}; |
| |
| /** |
| * The Ant-like exclusion patterns used to exclude JAR files from processing. The patterns must be relative to the |
| * directory given by the parameter {@link #archiveDirectory}. |
| * |
| * @since 1.1 |
| */ |
| @Parameter |
| private String[] excludes = {}; |
| |
| /** |
| * List of additional arguments to append to the jarsigner command line. Each argument should be specified as a |
| * separate element. For example, to specify the name of the signed jar, two elements are needed: |
| * <ul> |
| * <li>Alternative using the command line: {@code -Djarsigner.arguments="-signedjar,my-project_signed.jar"}</li> |
| * <li>Alternative using the Maven POM configuration:</li> |
| * </ul> |
| * <pre> |
| * {@code |
| * <configuration> |
| * <arguments> |
| * <argument>-signedjar</argument> |
| * <argument>my-project_signed.jar</argument> |
| * </arguments> |
| * </configuration> |
| * }</pre> |
| */ |
| @Parameter(property = "jarsigner.arguments") |
| private String[] arguments; |
| |
| /** |
| * Set to {@code true} to disable the plugin. |
| */ |
| @Parameter(property = "jarsigner.skip", defaultValue = "false") |
| private boolean skip; |
| |
| /** |
| * Controls processing of the main artifact produced by the project. |
| * |
| * @since 1.1 |
| */ |
| @Parameter(property = "jarsigner.processMainArtifact", defaultValue = "true") |
| private boolean processMainArtifact; |
| |
| /** |
| * Controls processing of project attachments. If enabled, attached artifacts that are no JAR/ZIP files will be |
| * automatically excluded from processing. |
| * |
| * @since 1.1 |
| */ |
| @Parameter(property = "jarsigner.processAttachedArtifacts", defaultValue = "true") |
| private boolean processAttachedArtifacts; |
| |
| /** |
| * Must be set to true if the password must be given via a protected |
| * authentication path such as a dedicated PIN reader. |
| * |
| * @since 1.3 |
| */ |
| @Parameter(property = "jarsigner.protectedAuthenticationPath", defaultValue = "false") |
| private boolean protectedAuthenticationPath; |
| |
| /** |
| * A set of artifact classifiers describing the project attachments that should be processed. This parameter is only |
| * relevant if {@link #processAttachedArtifacts} is <code>true</code>. If empty, all attachments are included. |
| * |
| * @since 1.2 |
| */ |
| @Parameter |
| private String[] includeClassifiers; |
| |
| /** |
| * A set of artifact classifiers describing the project attachments that should not be processed. This parameter is |
| * only relevant if {@link #processAttachedArtifacts} is <code>true</code>. If empty, no attachments are excluded. |
| * |
| * @since 1.2 |
| */ |
| @Parameter |
| private String[] excludeClassifiers; |
| |
| /** |
| * The Maven project. |
| */ |
| @Parameter(defaultValue = "${project}", readonly = true, required = true) |
| private MavenProject project; |
| |
| /** |
| * The Maven settings. |
| * |
| * @since 1.5 |
| */ |
| @Parameter(defaultValue = "${settings}", readonly = true, required = true) |
| private Settings settings; |
| |
| /** |
| * Location of the working directory. |
| * |
| * @since 1.3 |
| */ |
| @Parameter(defaultValue = "${project.basedir}") |
| private File workingDirectory; |
| |
| /** |
| * The current build session instance. This is used for |
| * toolchain manager API calls. |
| * |
| * @since 1.3 |
| */ |
| @Parameter(defaultValue = "${session}", readonly = true, required = true) |
| private MavenSession session; |
| |
| private final JarSigner jarSigner; |
| |
| /** |
| * To obtain a toolchain if possible. |
| * |
| * @since 1.3 |
| */ |
| private final ToolchainManager toolchainManager; |
| |
| /** |
| * @since 1.3.2 |
| */ |
| private final SecDispatcher securityDispatcher; |
| |
| protected AbstractJarsignerMojo( |
| JarSigner jarSigner, ToolchainManager toolchainManager, SecDispatcher securityDispatcher) { |
| this.jarSigner = jarSigner; |
| this.toolchainManager = toolchainManager; |
| this.securityDispatcher = securityDispatcher; |
| } |
| |
| @Override |
| public final void execute() throws MojoExecutionException { |
| if (this.skip) { |
| getLog().info(getMessage("disabled")); |
| return; |
| } |
| |
| validateParameters(); |
| |
| Toolchain toolchain = getToolchain(); |
| if (toolchain != null) { |
| getLog().info("Toolchain in maven-jarsigner-plugin: " + toolchain); |
| jarSigner.setToolchain(toolchain); |
| } |
| |
| List<File> archives = findJarfiles(); |
| processArchives(archives); |
| getLog().info(getMessage("processed", archives.size())); |
| } |
| |
| /** |
| * Finds all jar files, by looking at the Maven project and user configuration. |
| * |
| * @return a List of File objects |
| * @throws MojoExecutionException if it was not possible to build a list of jar files |
| */ |
| private List<File> findJarfiles() throws MojoExecutionException { |
| if (this.archive != null) { |
| // Only process this, but nothing more |
| return Arrays.asList(this.archive); |
| } |
| |
| List<File> archives = new ArrayList<>(); |
| if (processMainArtifact) { |
| getFileFromArtifact(this.project.getArtifact()).ifPresent(archives::add); |
| } |
| |
| if (processAttachedArtifacts) { |
| Collection<String> includes = new HashSet<>(); |
| if (includeClassifiers != null) { |
| includes.addAll(Arrays.asList(includeClassifiers)); |
| } |
| |
| Collection<String> excludes = new HashSet<>(); |
| if (excludeClassifiers != null) { |
| excludes.addAll(Arrays.asList(excludeClassifiers)); |
| } |
| |
| for (Artifact artifact : this.project.getAttachedArtifacts()) { |
| if (!includes.isEmpty() && !includes.contains(artifact.getClassifier())) { |
| continue; |
| } |
| |
| if (excludes.contains(artifact.getClassifier())) { |
| continue; |
| } |
| |
| getFileFromArtifact(artifact).ifPresent(archives::add); |
| } |
| } else { |
| if (verbose) { |
| getLog().info(getMessage("ignoringAttachments")); |
| } else { |
| getLog().debug(getMessage("ignoringAttachments")); |
| } |
| } |
| |
| if (archiveDirectory != null) { |
| String includeList = (includes != null) ? StringUtils.join(includes, ",") : null; |
| String excludeList = (excludes != null) ? StringUtils.join(excludes, ",") : null; |
| |
| try { |
| archives.addAll(FileUtils.getFiles(archiveDirectory, includeList, excludeList)); |
| } catch (IOException e) { |
| throw new MojoExecutionException("Failed to scan archive directory for JARs: " + e.getMessage(), e); |
| } |
| } |
| |
| return archives; |
| } |
| |
| /** |
| * Creates the jar signer request to be executed. |
| * |
| * @param archive the archive file to treat by jarsigner |
| * @return the request |
| * @throws MojoExecutionException if an exception occurs |
| * @since 1.3 |
| */ |
| protected abstract JarSignerRequest createRequest(File archive) throws MojoExecutionException; |
| |
| /** |
| * Gets a string representation of a {@code Commandline}. |
| * <p> |
| * This method creates the string representation by calling {@code commandLine.toString()} by default. |
| * </p> |
| * |
| * @param commandLine The {@code Commandline} to get a string representation of. |
| * @return The string representation of {@code commandLine}. |
| * @throws NullPointerException if {@code commandLine} is {@code null} |
| */ |
| protected String getCommandlineInfo(final Commandline commandLine) { |
| if (commandLine == null) { |
| throw new NullPointerException("commandLine"); |
| } |
| |
| String commandLineInfo = commandLine.toString(); |
| commandLineInfo = StringUtils.replace(commandLineInfo, this.storepass, "'*****'"); |
| return commandLineInfo; |
| } |
| |
| public String getStoretype() { |
| return storetype; |
| } |
| |
| public String getStorepass() { |
| return storepass; |
| } |
| |
| /** |
| * Checks whether the specified artifact is a ZIP file. |
| * |
| * @param artifact The artifact to check, may be <code>null</code>. |
| * @return <code>true</code> if the artifact looks like a ZIP file, <code>false</code> otherwise. |
| */ |
| private static boolean isZipFile(final Artifact artifact) { |
| return artifact != null && artifact.getFile() != null && JarSignerUtil.isZipFile(artifact.getFile()); |
| } |
| |
| /** |
| * Examines an Artifact and extract the File object pointing to the Artifact jar file. |
| * |
| * @param artifact the artifact to examine |
| * @return An Optional containing the File, or Optional.empty() if the File is not a jar file. |
| * @throws NullPointerException if {@code artifact} is {@code null} |
| */ |
| private Optional<File> getFileFromArtifact(final Artifact artifact) { |
| if (artifact == null) { |
| throw new NullPointerException("artifact"); |
| } |
| |
| if (isZipFile(artifact)) { |
| return Optional.of(artifact.getFile()); |
| } |
| |
| if (this.verbose) { |
| getLog().info(getMessage("unsupported", artifact)); |
| } else if (getLog().isDebugEnabled()) { |
| getLog().debug(getMessage("unsupported", artifact)); |
| } |
| return Optional.empty(); |
| } |
| |
| /** |
| * Pre-processes a given archive. |
| * |
| * @param archive The archive to process, must not be <code>null</code>. |
| * @throws MojoExecutionException if pre-processing failed |
| */ |
| protected void preProcessArchive(final File archive) throws MojoExecutionException { |
| // Default implementation does nothing |
| } |
| |
| /** |
| * Validate the user supplied configuration/parameters. |
| * |
| * @throws MojoExecutionException if the user supplied configuration make further execution impossible |
| */ |
| protected void validateParameters() throws MojoExecutionException { |
| // Default implementation does nothing |
| } |
| |
| /** |
| * Process (sign/verify) a list of archives. |
| * |
| * @param archives list of jar files to process |
| * @throws MojoExecutionException if an error occurs during the processing of archives |
| */ |
| protected void processArchives(List<File> archives) throws MojoExecutionException { |
| for (File file : archives) { |
| processArchive(file); |
| } |
| } |
| |
| /** |
| * Processes a given archive. |
| * |
| * @param archive The archive to process. |
| * @throws NullPointerException if {@code archive} is {@code null} |
| * @throws MojoExecutionException if processing {@code archive} fails |
| */ |
| protected final void processArchive(final File archive) throws MojoExecutionException { |
| if (archive == null) { |
| throw new NullPointerException("archive"); |
| } |
| |
| preProcessArchive(archive); |
| |
| if (this.verbose) { |
| getLog().info(getMessage("processing", archive)); |
| } else if (getLog().isDebugEnabled()) { |
| getLog().debug(getMessage("processing", archive)); |
| } |
| |
| JarSignerRequest request = createRequest(archive); |
| request.setVerbose(verbose); |
| request.setAlias(alias); |
| request.setArchive(archive); |
| request.setKeystore(keystore); |
| request.setStoretype(storetype); |
| request.setProviderArg(providerArg); |
| request.setProviderClass(providerClass); |
| request.setProviderName(providerName); |
| request.setWorkingDirectory(workingDirectory); |
| request.setMaxMemory(maxMemory); |
| request.setProtectedAuthenticationPath(protectedAuthenticationPath); |
| |
| // Preserves 'file.encoding' the plugin is executed with. |
| final List<String> additionalArguments = new ArrayList<>(); |
| |
| boolean fileEncodingSeen = false; |
| |
| if (this.arguments != null) { |
| for (final String argument : this.arguments) { |
| if (argument.trim().startsWith("-J-Dfile.encoding=")) { |
| fileEncodingSeen = true; |
| } |
| |
| additionalArguments.add(argument); |
| } |
| } |
| |
| if (!fileEncodingSeen) { |
| additionalArguments.add("-J-Dfile.encoding=" + ReaderFactory.FILE_ENCODING); |
| } |
| |
| // Adds proxy information. |
| if (this.settings != null |
| && this.settings.getActiveProxy() != null |
| && StringUtils.isNotEmpty(this.settings.getActiveProxy().getHost())) { |
| additionalArguments.add( |
| "-J-Dhttp.proxyHost=" + this.settings.getActiveProxy().getHost()); |
| additionalArguments.add( |
| "-J-Dhttps.proxyHost=" + this.settings.getActiveProxy().getHost()); |
| additionalArguments.add( |
| "-J-Dftp.proxyHost=" + this.settings.getActiveProxy().getHost()); |
| |
| if (this.settings.getActiveProxy().getPort() > 0) { |
| additionalArguments.add( |
| "-J-Dhttp.proxyPort=" + this.settings.getActiveProxy().getPort()); |
| additionalArguments.add( |
| "-J-Dhttps.proxyPort=" + this.settings.getActiveProxy().getPort()); |
| additionalArguments.add( |
| "-J-Dftp.proxyPort=" + this.settings.getActiveProxy().getPort()); |
| } |
| |
| if (StringUtils.isNotEmpty(this.settings.getActiveProxy().getNonProxyHosts())) { |
| additionalArguments.add("-J-Dhttp.nonProxyHosts=\"" |
| + this.settings.getActiveProxy().getNonProxyHosts() + "\""); |
| |
| additionalArguments.add("-J-Dftp.nonProxyHosts=\"" |
| + this.settings.getActiveProxy().getNonProxyHosts() + "\""); |
| } |
| } |
| |
| request.setArguments( |
| !additionalArguments.isEmpty() |
| ? additionalArguments.toArray(new String[additionalArguments.size()]) |
| : null); |
| |
| // Special handling for passwords through the Maven Security Dispatcher |
| request.setStorepass(decrypt(storepass)); |
| |
| try { |
| executeJarSigner(jarSigner, request); |
| } catch (JavaToolException e) { |
| throw new MojoExecutionException(getMessage("commandLineException", e.getMessage()), e); |
| } |
| } |
| |
| /** |
| * Executes jarsigner (execute signing or verification for a jar file). |
| * |
| * @param jarSigner the JarSigner execution interface |
| * @param request the JarSignerRequest with parameters JarSigner should use |
| * @throws JavaToolException if jarsigner could not be invoked |
| * @throws MojoExecutionException if the invocation of jarsigner succeeded, but returned a non-zero exit code |
| */ |
| protected abstract void executeJarSigner(JarSigner jarSigner, JarSignerRequest request) |
| throws JavaToolException, MojoExecutionException; |
| |
| protected String decrypt(String encoded) throws MojoExecutionException { |
| try { |
| return securityDispatcher.decrypt(encoded); |
| } catch (SecDispatcherException e) { |
| getLog().error("error using security dispatcher: " + e.getMessage(), e); |
| throw new MojoExecutionException("error using security dispatcher: " + e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Gets a message for a given key from the resource bundle backing the implementation. |
| * |
| * @param key the key of the message to return |
| * @param args arguments to format the message with |
| * @return the message with key {@code key} from the resource bundle backing the implementation |
| * @throws NullPointerException if {@code key} is {@code null} |
| * @throws java.util.MissingResourceException |
| * if there is no message available matching {@code key} or accessing |
| * the resource bundle fails |
| */ |
| String getMessage(final String key, final Object... args) { |
| if (key == null) { |
| throw new NullPointerException("key"); |
| } |
| |
| return new MessageFormat(ResourceBundle.getBundle("jarsigner").getString(key)).format(args); |
| } |
| |
| /** |
| * the part with ToolchainManager lookup once we depend on |
| * 2.0.9 (have it as prerequisite). Define as regular component field then. |
| * hint: check maven-compiler-plugin code |
| * |
| * @return Toolchain instance |
| */ |
| private Toolchain getToolchain() { |
| Toolchain tc = null; |
| if (toolchainManager != null) { |
| tc = toolchainManager.getToolchainFromBuildContext("jdk", session); |
| } |
| |
| return tc; |
| } |
| } |