Package org.glassfish.appclient.server.core

Source Code of org.glassfish.appclient.server.core.ApplicationSignedJARManager

/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2009-2014 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License").  You
* may not use this file except in compliance with the License.  You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt.  See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license."  If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above.  However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/

package org.glassfish.appclient.server.core;

import com.sun.enterprise.deploy.shared.ArchiveFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
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.JarFile;
import java.util.jar.Manifest;
import org.glassfish.api.deployment.DeploymentContext;
import org.glassfish.api.deployment.archive.ReadableArchive;
import org.glassfish.appclient.server.core.jws.JavaWebStartInfo;
import org.glassfish.appclient.server.core.jws.servedcontent.ASJarSigner;
import org.glassfish.appclient.server.core.jws.servedcontent.AutoSignedContent;
import org.glassfish.appclient.server.core.jws.servedcontent.FixedContent;
import org.glassfish.appclient.server.core.jws.servedcontent.StaticContent;
import org.glassfish.hk2.api.ServiceLocator;

/**
* Records information about JARs from an EAR that are used by an
* app client.  Although JARs can be signed by multiple certificates, this
* class ultimately associates each JAR with at most one alias with which
* it was signed.  (This is typically used by the Java Web Start support to
* group like-signed JARs into the same generated JNLP.  Java Web Start requires
* that all JARs listed in a single JNLP document be signed by the same cert or
* be unsigned.)  By organizing the signed JARs by the signing alias used for each,
* we can easily find all JARs signed by a given alias and then list them
* in the same generated JNLP.
* <p>
* A client class should instantiate the manager, then invoke addJar any number
* of times, then invoke aliasToContent to retrieve the map from each alias to
* the corresponding (relativeURI, StaticContent) pair.
* <p>
* If an added JAR is already signed by the developer we do not sign it again, but
* simply add it to the data structures.  If an added JAR is not signed we
* arrange for it to be auto-signed and the signed version will be the one
* served to Java Web Start requests.
*
* @author tjquinn
*/
public class ApplicationSignedJARManager {

    private final Map<URI,Collection<String>> relURIToSigningAliases =
                new HashMap<URI,Collection<String>>();
    private final Map<String,Collection<URI>> signingAliasToRelURIs =
                new HashMap<String,Collection<URI>>();

    /**
     * maps each alias to a map.  map entries link the relative URI (used as the
     * key in the Grizzly adapter's key-to-content map) to the StaticContent
     * instance for that JAR.  In this map each served JAR is associated with
     * only one alias, even if a JAR is signed by multiple certs.
     */
    private Map<String,Map<URI,StaticContent>> selectedAliasToContentMapping = null;

    private final ArchiveFactory archiveFactory;

    private final String autoSigningAlias;

    private final ASJarSigner jarSigner;

    private final URI EARDirectoryServerURI;

    private final DeploymentContext dc;

    private final AppClientDeployerHelper helper;

    private final Map<URI,StaticContent> relURIToContent =
            new HashMap<URI,StaticContent>();

    public ApplicationSignedJARManager(
            final String autoSigningAlias,
            final ASJarSigner jarSigner,
            final ServiceLocator habitat,
            final DeploymentContext dc,
            final AppClientDeployerHelper helper,
            final URI EARDirectoryServerURI,
            final URI EARDirectoryUserURI) {
        this.autoSigningAlias = autoSigningAlias;
        this.jarSigner = jarSigner;
        this.EARDirectoryServerURI = EARDirectoryServerURI;
        archiveFactory = habitat.getService(ArchiveFactory.class);
        this.dc = dc;
        this.helper = helper;
    }

    /**
     * Adds a JAR to the manager, returning the URI for the file to be served.
     * The URI within the anchor is derived from the absolute URI relative
     * to the EAR's anchor on the server.
     * @param absJARURI absolute URI of the unsigned file.
     * @return URI to the file to be served
     * @throws IOException
     */
    public URI addJAR(final URI absJARURI) throws IOException {
        final URI jarURIRelativeToApp = EARDirectoryServerURI.relativize(absJARURI);
        return addJAR(jarURIRelativeToApp, absJARURI);
    }

    /**
     * Adds a JAR to the manager, returning the URI to the file to be
     * served.  This might be an auto-signed file if the original JAR is
     * unsigned.
     * @param uriWithinAnchor relative URI to the JAR within the anchor directory for the app
     * @param jarURI URI to the JAR file in the app to be served
     * @return URI to the JAR file to serve (either the original file or an auto-signed copy of the original)
     * @throws IOException
     */
    public URI addJAR(final URI uriWithinAnchor, final URI absJARURI) throws IOException {
        /*
         * This method accomplishes three things:
         *
         * 1. Adds an entry to the map from relative URIs to the corresponding
         * static content for the JAR, creating an auto-signed content instance
         * if needed for an unsigned JAR.
         *
         * 2. Adds to the map from relative URI to aliases with which the JAR
         * is signed.
         *
         * 3. Adds to the map from alias to relative URIs signed with that alias.
         */
        Map.Entry<URI,StaticContent> result; // relative URI -> StaticContent

        final ReadableArchive arch = archiveFactory.openArchive(absJARURI);
        final Manifest archiveMF = arch.getManifest();
        if (archiveMF == null) {
            return null;
        }
        if ( ! isArchiveSigned(archiveMF)) {
            /*
             * The developer did not sign this JARs, so arrange for it to be
             * auto-signed.
             */

            result = autoSignedAppContentEntry(uriWithinAnchor, absJARURI);
            updateAliasToURIs(result.getKey(), autoSigningAlias);
            updateURIToAliases(result.getKey(), autoSigningAlias);
        } else {
            /*
             * The developer did sign this JAR, possibly with many certs.
             * For each cert add an association between the signing alias and
             * the JAR.
             */
            result = developerSignedAppContentEntry(absJARURI);
            Collection<String> aliasesUsedToSignJAR = new ArrayList<String>();
            for (Enumeration<String> entryNames = arch.entries("META-INF/");
                 entryNames.hasMoreElements(); ) {
                final String entryName = entryNames.nextElement();
                final String alias = signatureEntryName(entryName);
                updateURIToAliases(result.getKey(), alias);
            }
            addAliasToURIsEntry(result.getKey(), aliasesUsedToSignJAR);
        }
           
        arch.close();
        return result.getKey();
    }

    public Map<String,Map<URI,StaticContent>> aliasToContent() {
        if (selectedAliasToContentMapping == null) {
            selectedAliasToContentMapping = pruneMaps();
        }
        return selectedAliasToContentMapping;
    }

    private void addAliasToURIsEntry(final URI relURI,
            final Collection<String> aliases) throws IOException {
        relURIToSigningAliases.put(relURI, aliases);
        for (String alias : aliases) {
            updateAliasToURIs(relURI, alias);
        }
    }
   
    private void updateURIToAliases(final URI relURI,
            final String alias) throws IOException {
        Collection<String> aliasesForJAR = relURIToSigningAliases.get(relURI);
        if (aliasesForJAR == null) {
            aliasesForJAR = new ArrayList<String>();
            relURIToSigningAliases.put(relURI, aliasesForJAR);
        }
        aliasesForJAR.add(alias);
    }

    private void updateAliasToURIs(final URI relURI,
            final String alias) throws IOException {
        Collection<URI> urisForAlias = signingAliasToRelURIs.get(alias);
        if (urisForAlias == null) {
            urisForAlias = new ArrayList<URI>();
            signingAliasToRelURIs.put(alias, urisForAlias);
        }
        urisForAlias.add(relURI);
    }

    private Map.Entry<URI,StaticContent> developerSignedAppContentEntry(URI absURIToFile) {
        final URI jarURIRelativeToApp = EARDirectoryServerURI.relativize(absURIToFile);
        StaticContent content = relURIToContent.get(absURIToFile);
        if (content == null) {
            content = new FixedContent(new File(absURIToFile));
            relURIToContent.put(jarURIRelativeToApp, content);
        }
        return new AbstractMap.SimpleEntry<URI,StaticContent>(
            jarURIRelativeToApp, content);
    }

    public StaticContent staticContent(final URI jarURIRelativeToApp) {
        return relURIToContent.get(jarURIRelativeToApp);
    }
   
    /*
     * Returns information about an auto-signed JAR for a given absolute URI and
     * alias, creating the auto-signed content object and adding it to the
     * data structures if it is not already present.
     */
    private synchronized Map.Entry<URI,StaticContent> autoSignedAppContentEntry(
            final URI jarURIRelativeToApp,
            final URI absURIToFile) throws FileNotFoundException {

        StaticContent content = relURIToContent.get(jarURIRelativeToApp);
        if (content == null) {
            final File unsignedFile = new File(absURIToFile);
            final File signedFile = signedFileForLib(jarURIRelativeToApp, unsignedFile);
            content = new AutoSignedContent(unsignedFile, signedFile, autoSigningAlias, jarSigner, jarURIRelativeToApp.toASCIIString(),
                    helper.appName());
            relURIToContent.put(jarURIRelativeToApp, content);
        } else {
            if (content instanceof AutoSignedContent) {
                content = AutoSignedContent.class.cast(content);
            } else {
                throw new RuntimeException(content.toString() + " != AutoSignedContent");
            }
        }
        return new AbstractMap.SimpleEntry(jarURIRelativeToApp, content);
    }

    private File signedFileForLib(final URI relURI, final File unsignedFile) {
        return JavaWebStartInfo.signedFileForProvidedAppFile(relURI, unsignedFile, helper, dc);
    }

    /**
     * Returns the signature file name (no path, no suffix) if the specified
     * entry name matches the pattern of a signature file in a JAR.
     * @param entryName name to check
     * @return signature file name; null if the entry name does not match the pattern
     */
    private String signatureEntryName(final String entryName) {
        final int firstSlash = entryName.indexOf('/');
        final int lastSlash = entryName.lastIndexOf('/');
        return ((entryName.startsWith("META-INF/")
                && firstSlash == lastSlash && firstSlash != -1)
                && (entryName.endsWith(".SF")))

                ? entryName.substring(firstSlash + 1, entryName.indexOf(".SF"))
                : null;
    }

    private boolean isArchiveSigned(final Manifest archiveMF) throws IOException {
        /*
         * Signature files are *.SF, but looking through all the entries for
         * ones that match *.SF could be expensive if there are many entries.
         * Instead check the manifest to
         * see if it contains per-entry attributes and, if so, if the first
         * entry has a x-Digest-y entry-level attribute.
         */
        final Map<String,Attributes> perEntryAttrs = archiveMF.getEntries();
        boolean jarIsSigned = false;
        for (Map.Entry<String,Attributes> entry : perEntryAttrs.entrySet()) {
            for (Object attrKey : entry.getValue().keySet()) {
                if (attrKey.toString().contains("-Digest-") || attrKey.toString().contains("-Digest:")) {
                    jarIsSigned = true;
                    break;
                }
            }
            /*
             * We need to look only at the first entry because every entry
             * of a JAR file is recorded as signed in the manifest.
             */
            break;
        }
        return jarIsSigned;
    }

    private Map<String,Map<URI,StaticContent>> pruneMaps() {
        /*
         * We'll eventually generate possibly multiple JNLP documents, one for each
         * different signing cert and each listing the JARs signed using that cert.
         * Java Web Start prompts end users for each untrusted cert. that was
         * used to sign the JARs in a JNLP.  During deployment (which is when this code runs)
         * we cannot tell what certs or trusted authorities might be on an
         * end-users's system.  So we would like to minimize the number of
         * different JNLPs which might help reduce the number of prompts the
         * user will see.
         *
         * We have a relationship between JARs and signing aliases.  Ideally
         * we'd truly minimize the number of aliases but that's a hard (i.e.,
         * computationally complex) problem and it's unlikely that real apps will contain
         * JARs signed by large numbers of different certs.  So we'll do as
         * good a job as we can, but quickly.
         *
         * If a JAR is signed by exactly one cert then we must include that cert
         * and we might as well associate any other JARs signed by that cert
         * and others with that cert.  Then we'll process any
         * unprocessed JARs by choosing for each the alias with which it was
         * signed with the largest number of other JARs also signed by that
         * alias.  This is not guaranteed to be optimal but it should be
         * pretty good and will be fast.
         */

        final Set<URI> processedJARs = new HashSet<URI>();
        final Map<String,Map<URI,StaticContent>> selectedAliases =
                new HashMap<String,Map<URI,StaticContent>>();

        for (Map.Entry<URI,Collection<String>> entry : relURIToSigningAliases.entrySet()) {
            if ( ! processedJARs.contains(entry.getKey())) {
                if (entry.getValue().size() == 1) {
                    processURI(processedJARs, selectedAliases,
                            entry.getKey(), entry.getValue().iterator().next());
                }
            }
        }

        /*
         * We've handled all JARs that have just one signing alias.  Now process
         * any remaining JARs.
         */
        for (Map.Entry<URI,Collection<String>> entry : relURIToSigningAliases.entrySet()) {
            if ( ! processedJARs.contains(entry.getKey())) {
                processURI(processedJARs, selectedAliases,
                        entry.getKey(), entry.getValue());
            }
        }
        return selectedAliases;
    }

    private void processURI(final Set<URI> processedJARs,
            final Map<String,Map<URI,StaticContent>> selectedAliases,
            final URI relURI,
            final String alias) {
        Map<URI,StaticContent> urisForSelectedAlias = selectedAliases.get(alias);
        if (urisForSelectedAlias == null) {
            urisForSelectedAlias = new HashMap<URI,StaticContent>();
            selectedAliases.put(alias, urisForSelectedAlias);
        }
        /*
         * Add this URI to the URIs to be associated with the specified alias.
         */
        urisForSelectedAlias.put(relURI, relURIToContent.get(relURI));

        /*
         * Record that we've processed this URI so we don't do so again.
         */
        processedJARs.add(relURI);

        /*
         * Now that we know we need to handle this alias, mark all other JARs
         * that are associated with this alias (and perhaps others) to be
         * finally grouped with this alias alone.
         */
        for (URI otherURI : signingAliasToRelURIs.get(alias)) {
            urisForSelectedAlias.put(otherURI, relURIToContent.get(otherURI));
            processedJARs.add(otherURI);
        }
    }

    private void processURI(final Set<URI> processedJARs,
            final Map<String,Map<URI,StaticContent>> selectedAliases,
            final URI uri,
            final Collection<String> aliases) {
        /*
         * The algorithm we use to choose which of the multiple aliases to use
         * for this JAR could be anything.  We'll just choose the first one.
         */
        processURI(processedJARs, selectedAliases, uri, aliases.iterator().next());
    }
}
TOP

Related Classes of org.glassfish.appclient.server.core.ApplicationSignedJARManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.