org.eclipse.cbi.maven.plugins.macsigner.SignMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.cbi.maven.plugins.macsigner.SignMojo.java

Source

/*******************************************************************************
 * Copyright (c) 2013, 2014 Eclipse Foundation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Caroline McQuatt, Mike Lim - initial implementation
 *******************************************************************************/

package org.eclipse.cbi.maven.plugins.macsigner;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.http.NoHttpResponseException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.codehaus.plexus.util.FileUtils;
import org.eclipse.cbi.common.signing.Signer;

/**
 * Signs project main and attached artifact using
 * <a href="http://wiki.eclipse.org/IT_Infrastructure_Doc#Sign_my_plugins.2FZIP_files.3F">Eclipse macsigner webservice</a>.
 *
 * @goal sign
 * @phase package
 * @requiresProject
 * @description runs the eclipse signing process
 */
public class SignMojo extends AbstractMojo {
    /**
     * The signing service URL for signing Mac binaries
     *
     * <p>The signing service should return a signed zip file. Containing
     * the Mac *.app directory.</p>
     *
     * <p>The Official Eclipse signer service URL as described in the
     * <a href="http://wiki.eclipse.org/IT_Infrastructure_Doc#Sign_my_plugins.2FZIP_files.3F">
     * wiki</a>.</p>
     *
     * <p><b>Configuration via Maven commandline</b></p>
     * <pre>-Dcbi.macsigner.signerUrl=http://localhost/macsign.php</pre>
     *
     * <p><b>Configuration via pom.xml</b></p>
     * <pre>{@code
     * <configuration>
     *   <signerUrl>http://localhost/macsign</signerUrl>
     * </configuration>
     * }</pre>
     *
     * @parameter property="cbi.macsigner.signerUrl" default-value="http://build.eclipse.org:31338/macsign.php"
     * @required
     * @since 1.0.4
     */
    private String signerUrl;

    /**
     * Maven build directory
     *
     * @parameter property="project.build.directory"
     * @readonly
     * @since 1.0.4
     */
    private File workdir;

    /**
     * A list of full paths to the Mac directory *.app
     *
     * <p>If configured only these executables will be signed.</p>
     * <p><b><i>
     *    NOTE: If this is configured "baseSearchDir" and "fileNames"
     *    do NOT need to be configured.
     * </i></b></p>
     *
     * <p><b>Configuration via pom.xml</b></p>
     * <pre>{@code
     * <configuration>
     *   <signFiles>
     *     <signFile>}${project.build.directory}/products/org.eclipse.sdk.ide/macosx/cocoa/x86/eclipse/Eclipse.app{@code</signFile>
     *     <signFile>}${project.build.directory}/products/org.eclipse.sdk.ide/macosx/cocoa/x86_64/eclipse/Eclipse.app{@code</signFile>
     *   </signFiles>
     * </configuration>
     * }</pre>
     *
     * @parameter property="signFiles"
     * @since 1.0.4
     */
    private String[] signFiles;

    /**
     * The base directory to search for executables to sign
     *
     * <p>If NOT configured baseSearchDir is ${project.build.directory}/products/</p>
     *
     * @parameter property="baseSearchDir" default-value="${project.build.directory}/products/"
     * @since 1.0.4
     */
    private String baseSearchDir;

    /**
     * A list of *.app filenames to sign
     *
     * <p>If NOT configured 'Eclipse.app' is signed.</p>
     *
     * @parameter property="fileNames"
     * @since 1.0.4
     */
    private String[] fileNames;

    /**
     * Continue the build even if signing fails
     *
     * <p><b>Configuration via Maven commandline</b></p>
     * <pre>-DcontinueOnFail=true</pre>
     *
     * <p><b>Configuration via pom.xml</b></p>
     * <pre>{@code
     * <configuration>
     *   <continueOnFail>true</continueOnFail>
     * </configuration>
     * }</pre>
     *
     * @parameter property="continueOnFail" default-value="false"
     * @since 1.0.5
     */
    private boolean continueOnFail;

    /**
     * Number of times to retry signing if server fails to sign
     *
     * @parameter property="retryLimit" default-value="3"
     * @since 1.1.0
     */
    private int retryLimit;

    /**
     * Number of seconds to wait before retrying to sign
     *
     * @parameter property="retryTimer" default-value="30"
     * @since 1.1.0
     */
    private int retryTimer;

    /**
     * List of executable files on the .app file to be signed.
     */
    private static ArrayList<String> executableFiles = new ArrayList<String>();

    /**
     * Part of the unsigned zip file name.
     */
    private static final String UNSIGNED_ZIP_FILE_NAME = "app_unsigned";

    /**
     * Part of the signed zip file name.
     */
    private static final String SIGNED_ZIP_FILE_NAME = "app_signed";

    /**
     * The zip file extension.
     */
    private static final String ZIP_EXT = ".zip";

    /**
     * The number of byte written to the output stream during zip and unzip.
     */
    private static final int BUFFER_SIZE = 1024;

    @Override
    public void execute() throws MojoExecutionException {
        //app paths are configured
        if (signFiles != null && !(signFiles.length == 0)) {
            for (String path : signFiles) {
                signArtifact(new File(path));
            }
        } else { //perform search
            if (fileNames == null || fileNames.length == 0) {
                fileNames = new String[1];
                fileNames[0] = "Eclipse.app";
            }

            File searchDir = new File(baseSearchDir);
            getLog().debug("Searching: " + searchDir);
            traverseDirectory(searchDir);
        }
    }

    /**
     * Recursive method. Searches the base directory for files to sign.
     * @param dir
     * @throws MojoExecutionException
     */
    private void traverseDirectory(File dir) throws MojoExecutionException {
        if (dir.isDirectory()) {
            getLog().debug("searching " + dir.getAbsolutePath());
            for (File file : dir.listFiles()) {
                if (file.isFile()) {
                    continue;
                } else if (file.isDirectory()) {
                    boolean isSigned = false;
                    String fileName = file.getName();
                    for (String allowedName : fileNames) {
                        if (fileName.equals(allowedName)) {
                            signArtifact(file); // signs the file
                            isSigned = true;
                            break;
                        }
                    }
                    if (!isSigned) // do not search directories that are already signed
                    {
                        traverseDirectory(file);
                    }
                }
            }
        } else {
            getLog().error("Internal error. " + dir + " is not a directory.");
        }
    }

    /**
     * Decompresses zip files.
     * @param zipFile           The zip file to decompress.
     * @throws IOException
     * @throws MojoExecutionException
     */
    private static void unZip(File zipFile, File output_dir) throws IOException, MojoExecutionException {

        ZipArchiveInputStream zis = new ZipArchiveInputStream(new FileInputStream(zipFile));
        ZipArchiveEntry ze;
        String name, parent;
        try {
            ze = zis.getNextZipEntry();
            // check for at least one zip entry
            if (ze == null) {
                throw new MojoExecutionException("Could not decompress " + zipFile);
            }

            while (ze != null) {
                name = ze.getName();

                //make directories
                if (ze.isDirectory()) {
                    mkdirs(output_dir, name);
                } else {
                    parent = getParentDirAbsolutePath(name);
                    mkdirs(output_dir, parent);

                    File outFile = new File(output_dir, name);
                    outFile.createNewFile();

                    // check for match in executable list
                    if (executableFiles.contains(name)) {
                        Files.setPosixFilePermissions(outFile.toPath(),
                                PosixFilePermissions.fromString("rwxr-x---"));
                    }

                    FileOutputStream fos = new FileOutputStream(outFile);

                    copyInputStreamToOutputStream(zis, fos);
                    fos.close();
                }
                ze = zis.getNextZipEntry();
            }
        } finally {
            zis.close();
        }
    }

    /**
     * Helper method to create a new file and make all of the necessary directories.
     * @param outdir            The parent of the new file.
     * @param path              The child path of the new file relative to the parent.
     */
    private static void mkdirs(File outdir, String path) {
        File d = new File(outdir, path);
        if (!d.exists())
            d.mkdirs();
    }

    /**
     * Creates a zip file.
     * @param dir                   The Directory of the files to be zipped.
     * @param zip                   An output stream to write the file
     * @throws IOException
     */
    private void createZip(File dir, ZipArchiveOutputStream zip) throws IOException {
        Deque<File> dir_stack = new LinkedList<File>();
        dir_stack.push(dir);

        // base path is the parent of the "Application.app" folder
        // it will be used to make "Application.app" the top-level folder in the zip
        String base_path = getParentDirAbsolutePath(dir);

        // verify that "dir" actually id the ".app" folder
        if (!dir.getName().endsWith(".app"))
            throw new IOException("Please verify the configuration. Directory does not end with '.app': " + dir);

        while (!dir_stack.isEmpty()) {

            File file = dir_stack.pop();
            File[] files = file.listFiles();

            for (File f : files) {
                String name = f.getAbsolutePath().substring(base_path.length());
                getLog().debug("Found: " + name);

                if (f.isFile() && isInContentsFolder(name)) {
                    getLog().debug("Adding to zip file for signing: " + f);

                    ZipArchiveEntry entry = new ZipArchiveEntry(name);
                    zip.putArchiveEntry(entry);

                    if (f.canExecute()) {
                        //work around to track the relative file names
                        // of those that need to be set as executable on unZip
                        executableFiles.add(name);
                    }
                    InputStream is = new FileInputStream(f);
                    copyInputStreamToOutputStream(is, zip);

                    is.close();
                    zip.closeArchiveEntry();
                } else if (f.isDirectory() && isInContentsFolder(name)) { //add directory entry
                    dir_stack.push(f);
                } else {
                    getLog().debug(f + " was not included in the zip file to be signed.");
                }
            }
        }
    }

    private static boolean isInContentsFolder(String name) {
        String[] segments = name.split("/");
        return segments.length > 1 && segments[0].endsWith(".app") && segments[1].equals("Contents");
    }

    /**
     * Helper method. Returns the absolute path of a file's parent.
     * @param dir
     * @return          The absolute path of a file's parent.
     *                  Returns the empty string if there is no parent directory.
     */
    private static String getParentDirAbsolutePath(File file) {
        return getParentDirAbsolutePath(file.getAbsolutePath());
    }

    /**
     * Helper method. Returns the absolute path of a file's parent.
     * @param name
     * @return          The absolute path of a file's parent.
     *                  Returns the empty string if there is no parent directory.
     */
    private static String getParentDirAbsolutePath(String name) {
        int index = name.lastIndexOf(File.separator);
        return name.substring(0, index + 1);
    }

    /**
     * Helper method. Writes bytes from an InputStream to an OutputStream.
     * @param fis                   The InputStream.
     * @param zip                   The OutputStream.
     * @throws IOException
     */
    private static void copyInputStreamToOutputStream(InputStream fis, OutputStream zip) throws IOException {
        byte[] buff = new byte[BUFFER_SIZE];

        while (true) {
            int r_count = fis.read(buff);
            if (r_count < 0) {
                break;
            }
            zip.write(buff, 0, r_count);
        }
    }

    /**
     * Signs the file.
     * @param file
     * @throws MojoExecutionException
     */
    protected void signArtifact(File file) throws MojoExecutionException {
        try {
            if (!file.isDirectory()) {
                getLog().warn(file + " is a not a directory, the artifact is not signed.");
                return; // Expecting the .app directory
            }

            workdir.mkdirs();

            //zipping the directory
            getLog().debug("Building zip: " + file);
            File zipFile = File.createTempFile(UNSIGNED_ZIP_FILE_NAME, ZIP_EXT, workdir);
            ZipArchiveOutputStream zos = new ZipArchiveOutputStream(new FileOutputStream(zipFile));

            createZip(file, zos);
            zos.finish();
            zos.close();

            final long start = System.currentTimeMillis();

            String base_path = getParentDirAbsolutePath(file);
            File zipDir = new File(base_path);
            File tempSigned = File.createTempFile(SIGNED_ZIP_FILE_NAME, ZIP_EXT, workdir);
            File tempSignedCopy = new File(base_path + File.separator + tempSigned.getName());

            if (tempSignedCopy.exists()) {
                String msg = "Could not copy signed file because a file with the same name already exists: "
                        + tempSignedCopy;

                if (continueOnFail) {
                    getLog().warn(msg);
                } else {
                    throw new MojoExecutionException(msg);
                }
            }
            tempSignedCopy.createNewFile();

            FileUtils.copyFile(tempSigned, tempSignedCopy);

            try {
                signFile(zipFile, tempSigned);
                if (!tempSigned.canRead() || tempSigned.length() <= 0) {
                    String msg = "Could not sign artifact " + file;

                    if (continueOnFail) {
                        getLog().warn(msg);
                    } else {
                        throw new MojoExecutionException(msg);
                    }
                }

                // unzipping response
                getLog().debug("Decompressing zip: " + file);
                unZip(tempSigned, zipDir);
            } finally {
                if (!zipFile.delete()) {
                    getLog().warn("Temporary file failed to delete: " + zipFile);
                }
                if (!tempSigned.delete()) {
                    getLog().warn("Temporary file failed to delete: " + tempSigned);
                }
                if (!tempSignedCopy.delete()) {
                    getLog().warn("Temporary file failed to delete: " + tempSignedCopy);
                }
            }

            getLog().info("Signed " + file + " in " + ((System.currentTimeMillis() - start) / 1000) + " seconds.");
        } catch (IOException e) {
            String msg = "Could not sign file " + file + ": " + e.getMessage();

            if (continueOnFail) {
                getLog().warn(msg);
            } else {
                throw new MojoExecutionException(msg, e);
            }
        } finally {
            executableFiles.clear();
        }
    }

    /**
     * helper to send the file to the signing service
     * @param source file to send
     * @param target file to copy response to
     * @throws IOException
     * @throws MojoExecutionException
     */
    private void signFile(File source, File target) throws IOException, MojoExecutionException {
        int retry = 0;

        NoHttpResponseException serverException = null;
        while (retry++ <= retryLimit) {
            try {
                Signer.signFile(source, target, signerUrl);
                return;
            } catch (NoHttpResponseException e) {
                getLog().debug("Server error while signing: " + e.getMessage(), e);
                if (serverException == null)
                    serverException = e;
                else
                    serverException.addSuppressed(e);

                if (retry <= retryLimit) {
                    getLog().warn("Failed to sign with server. Retrying...");
                    try {
                        TimeUnit.SECONDS.sleep(retryTimer);
                    } catch (InterruptedException ie) {
                        // Do nothing
                    }
                }
            }
        }

        // If we make it here then signing has failed.
        throw new MojoExecutionException("Failed to sign file.", serverException);
    }
}