1 /*
   2  * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.oracle.tools.packager.mac;
  27 
  28 import com.oracle.tools.packager.AbstractBundler;
  29 import com.oracle.tools.packager.BundlerParamInfo;
  30 import com.oracle.tools.packager.StandardBundlerParam;
  31 import com.oracle.tools.packager.Log;
  32 import com.oracle.tools.packager.ConfigException;
  33 import com.oracle.tools.packager.IOUtils;
  34 import com.oracle.tools.packager.UnsupportedPlatformException;
  35 
  36 import java.io.ByteArrayOutputStream;
  37 import java.io.File;
  38 import java.io.IOException;
  39 import java.io.PrintStream;
  40 import java.nio.file.Files;
  41 import java.nio.file.Path;
  42 import java.nio.file.attribute.PosixFilePermission;
  43 import java.text.MessageFormat;
  44 import java.util.ArrayList;
  45 import java.util.Arrays;
  46 import java.util.Collection;
  47 import java.util.EnumSet;
  48 import java.util.LinkedHashSet;
  49 import java.util.List;
  50 import java.util.Map;
  51 import java.util.ResourceBundle;
  52 import java.util.Set;
  53 import java.util.concurrent.atomic.AtomicReference;
  54 import java.util.function.Consumer;
  55 import java.util.regex.Matcher;
  56 import java.util.regex.Pattern;
  57 
  58 import static com.oracle.tools.packager.StandardBundlerParam.*;
  59 
  60 public abstract class MacBaseInstallerBundler extends AbstractBundler {
  61 
  62     private static final ResourceBundle I18N =
  63             ResourceBundle.getBundle(MacBaseInstallerBundler.class.getName());
  64 
  65     //This could be generalized more to be for any type of Image Bundler
  66     public static final BundlerParamInfo<MacAppBundler> APP_BUNDLER = new StandardBundlerParam<>(
  67             I18N.getString("param.app-bundler.name"),
  68             I18N.getString("param.app-bundle.description"),
  69             "mac.app.bundler",
  70             MacAppBundler.class,
  71             params -> new MacAppBundler(),
  72             (s, p) -> null);
  73 
  74     public final BundlerParamInfo<File> APP_IMAGE_BUILD_ROOT = new StandardBundlerParam<>(
  75             I18N.getString("param.app-image-build-root.name"),
  76             I18N.getString("param.app-image-build-root.description"),
  77             "mac.app.imageRoot",
  78             File.class,
  79             params -> {
  80                 File imageDir = IMAGES_ROOT.fetchFrom(params);
  81                 if (!imageDir.exists()) imageDir.mkdirs();
  82                 try {
  83                     return Files.createTempDirectory(imageDir.toPath(), "image-").toFile();
  84                 } catch (IOException e) {
  85                     return new File(imageDir, getID()+ ".image");
  86                 }
  87             },
  88             (s, p) -> new File(s));
  89 
  90     public static final StandardBundlerParam<File> MAC_APP_IMAGE = new StandardBundlerParam<>(
  91             I18N.getString("param.app-image.name"),
  92             I18N.getString("param.app-image.description"),
  93             "mac.app.image",
  94             File.class,
  95             params -> null,
  96             (s, p) -> new File(s));
  97 
  98 
  99     public static final BundlerParamInfo<MacDaemonBundler> DAEMON_BUNDLER = new StandardBundlerParam<>(
 100             I18N.getString("param.daemon-bundler.name"),
 101             I18N.getString("param.daemon-bundler.description"),
 102             "mac.daemon.bundler",
 103             MacDaemonBundler.class,
 104             params -> new MacDaemonBundler(),
 105             (s, p) -> null);
 106 
 107 
 108     public final BundlerParamInfo<File> DAEMON_IMAGE_BUILD_ROOT = new StandardBundlerParam<>(
 109             I18N.getString("param.daemon-image-build-root.name"),
 110             I18N.getString("param.daemon-image-build-root.description"),
 111             "mac.daemon.image",
 112             File.class,
 113             params -> {
 114                 File imageDir = IMAGES_ROOT.fetchFrom(params);
 115                 if (!imageDir.exists()) imageDir.mkdirs();
 116                 return new File(imageDir, getID()+ ".daemon");
 117             },
 118             (s, p) -> new File(s));
 119 
 120 
 121     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
 122             I18N.getString("param.config-root.name"),
 123             I18N.getString("param.config-root.description"),
 124             "configRoot",
 125             File.class,
 126             params -> {
 127                 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
 128                 imagesRoot.mkdirs();
 129                 return imagesRoot;
 130             },
 131             (s, p) -> null);
 132 
 133     public static final BundlerParamInfo<String> SIGNING_KEY_USER = new StandardBundlerParam<>(
 134             I18N.getString("param.signing-key-name.name"),
 135             I18N.getString("param.signing-key-name.description"),
 136             "mac.signing-key-user-name",
 137             String.class,
 138             params -> "",
 139             null);
 140 
 141     public static final BundlerParamInfo<String> SIGNING_KEYCHAIN = new StandardBundlerParam<>(
 142             I18N.getString("param.signing-keychain.name"),
 143             I18N.getString("param.signing-keychain.description"),
 144             "mac.signing-keychain",
 145             String.class,
 146             params -> "",
 147             null);
 148 
 149     public static final BundlerParamInfo<String> INSTALLER_NAME = new StandardBundlerParam<> (
 150             I18N.getString("param.installer-name.name"),
 151             I18N.getString("param.installer-name.description"),
 152             "mac.installerName",
 153             String.class,
 154             params -> {
 155                 String nm = APP_NAME.fetchFrom(params);
 156                 if (nm == null) return null;
 157 
 158                 String version = VERSION.fetchFrom(params);
 159                 if (version == null) {
 160                     return nm;
 161                 } else {
 162                     return nm + "-" + version;
 163                 }
 164             },
 165             (s, p) -> s);
 166 
 167     public static File getPredefinedImage(Map<String, ? super Object> p) {
 168         File applicationImage = null;
 169         if (MAC_APP_IMAGE.fetchFrom(p) != null) {
 170             applicationImage = MAC_APP_IMAGE.fetchFrom(p);
 171             Log.debug("Using App Image from " + applicationImage);
 172             if (!applicationImage.exists()) {
 173                 throw new RuntimeException(
 174                         MessageFormat.format(I18N.getString("message.app-image-dir-does-not-exist"), MAC_APP_IMAGE.getID(), applicationImage.toString()));
 175             }
 176         }
 177         return applicationImage;
 178     }
 179 
 180     protected void validateAppImageAndBundeler(Map<String, ? super Object> params) throws ConfigException, UnsupportedPlatformException {
 181         if (MAC_APP_IMAGE.fetchFrom(params) != null) {
 182             File applicationImage = MAC_APP_IMAGE.fetchFrom(params);
 183             if (!applicationImage.exists()) {
 184                 throw new ConfigException(
 185                         MessageFormat.format(I18N.getString("message.app-image-dir-does-not-exist"), MAC_APP_IMAGE.getID(), applicationImage.toString()),
 186                         MessageFormat.format(I18N.getString("message.app-image-dir-does-not-exist.advice"), MAC_APP_IMAGE.getID()));
 187             }
 188             if (APP_NAME.fetchFrom(params) == null) {
 189                 throw new ConfigException(
 190                         I18N.getString("message.app-image-requires-app-name"),
 191                         I18N.getString("message.app-image-requires-app-name.advice"));
 192             }
 193             if (IDENTIFIER.fetchFrom(params) == null) {
 194                 throw new ConfigException(
 195                         I18N.getString("message.app-image-requires-identifier"),
 196                         I18N.getString("message.app-image-requires-identifier.advice"));
 197             }
 198         } else {
 199             APP_BUNDLER.fetchFrom(params).doValidate(params);
 200         }
 201     }
 202 
 203     protected File prepareAppBundle(Map<String, ? super Object> p) {
 204         File predefinedImage = getPredefinedImage(p);
 205         if (predefinedImage != null) {
 206             return predefinedImage;
 207         }
 208 
 209         File appImageRoot = APP_IMAGE_BUILD_ROOT.fetchFrom(p);
 210         return APP_BUNDLER.fetchFrom(p).doBundle(p, appImageRoot, true);
 211     }
 212 
 213     protected File prepareDaemonBundle(Map<String, ? super Object> p) {
 214         File daemonImageRoot = DAEMON_IMAGE_BUILD_ROOT.fetchFrom(p);
 215         return DAEMON_BUNDLER.fetchFrom(p).doBundle(p, daemonImageRoot, true);
 216     }
 217 
 218     public static void signAppBundle(Map<String, ? super Object> params, File appLocation, String signingIdentity, String identifierPrefix) throws IOException {
 219         signAppBundle(params, appLocation, signingIdentity, identifierPrefix, null, null);
 220     }
 221 
 222     public static void signAppBundle(Map<String, ? super Object> params, File appLocation, String signingIdentity, String identifierPrefix, String entitlementsFile, String inheritedEntitlements) throws IOException {
 223         AtomicReference<IOException> toThrow = new AtomicReference<>();
 224         String appExecutable = "/Contents/MacOS/" + APP_NAME.fetchFrom(params);
 225         String keyChain = SIGNING_KEYCHAIN.fetchFrom(params);
 226         
 227         // sign all dylibs and jars
 228         Files.walk(appLocation.toPath())
 229                 // while we are searching let's fix permissions
 230                 .peek(path -> {
 231                     try {
 232                         Set<PosixFilePermission> pfp = Files.getPosixFilePermissions(path);
 233                         if (!pfp.contains(PosixFilePermission.OWNER_WRITE)) {
 234                             pfp = EnumSet.copyOf(pfp);
 235                             pfp.add(PosixFilePermission.OWNER_WRITE);
 236                             Files.setPosixFilePermissions(path, pfp);
 237                         }
 238                     } catch (IOException e) {
 239                         Log.debug(e);
 240                     }
 241                 })
 242                 .filter(p -> Files.isRegularFile(p) && 
 243                                 !(p.toString().contains("/Contents/MacOS/libjli.dylib")
 244                                   || p.toString().contains("/Contents/MacOS/JavaAppletPlugin")
 245                                   || p.toString().endsWith(appExecutable))
 246                 ).forEach(p -> {
 247             //noinspection ThrowableResultOfMethodCallIgnored
 248             if (toThrow.get() != null) return;
 249 
 250             List<String> args = new ArrayList<>();
 251             args.addAll(Arrays.asList("codesign",
 252                     "-s", signingIdentity, // sign with this key
 253                     "--prefix", identifierPrefix, // use the identifier as a prefix
 254                     "-vvvv"));
 255             if (entitlementsFile != null && 
 256                     (p.toString().endsWith(".jar")
 257                       || p.toString().endsWith(".dylib"))) 
 258             {
 259                 args.add("--entitlements");
 260                 args.add(entitlementsFile); // entitlements
 261             } else if (inheritedEntitlements != null && Files.isExecutable(p)) {
 262                 args.add("--entitlements");
 263                 args.add(inheritedEntitlements); // inherited entitlements for executable processes
 264             }
 265             if (keyChain != null && !keyChain.isEmpty()) {
 266                 args.add("--keychain");
 267                 args.add(keyChain);
 268             }
 269             args.add(p.toString());
 270 
 271             try {
 272                 Set<PosixFilePermission> oldPermissions = Files.getPosixFilePermissions(p);
 273                 File f = p.toFile();
 274                 f.setWritable(true, true);
 275 
 276                 ProcessBuilder pb = new ProcessBuilder(args);
 277                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 278 
 279                 Files.setPosixFilePermissions(p, oldPermissions);
 280             } catch (IOException ioe) {
 281                 toThrow.set(ioe);
 282             }
 283         });
 284 
 285         IOException ioe = toThrow.get();
 286         if (ioe != null) {
 287             throw ioe;
 288         }
 289 
 290         // sign all plugins and frameworks
 291         Consumer<? super Path> signIdentifiedByPList = path -> {
 292             //noinspection ThrowableResultOfMethodCallIgnored
 293             if (toThrow.get() != null) return;
 294 
 295             try {
 296                 List<String> args = new ArrayList<>();
 297                 args.addAll(Arrays.asList("codesign",
 298                         "-s", signingIdentity, // sign with this key
 299                         "--prefix", identifierPrefix, // use the identifier as a prefix
 300                         "-vvvv"));
 301                 if (keyChain != null && !keyChain.isEmpty()) {
 302                     args.add("--keychain");
 303                     args.add(keyChain);
 304                 }
 305                 args.add(path.toString());
 306                 ProcessBuilder pb = new ProcessBuilder(args);
 307                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 308 
 309                 args = new ArrayList<>();
 310                 args.addAll(Arrays.asList("codesign",
 311                         "-s", signingIdentity, // sign with this key
 312                         "--prefix", identifierPrefix, // use the identifier as a prefix
 313                         "-vvvv"));
 314                 if (keyChain != null && !keyChain.isEmpty()) {
 315                     args.add("--keychain");
 316                     args.add(keyChain);
 317                 }
 318                 args.add(path.toString() + "/Contents/_CodeSignature/CodeResources");
 319                 pb = new ProcessBuilder(args);
 320                 IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 321             } catch (IOException e) {
 322                 toThrow.set(e);
 323             }
 324         };
 325 
 326         Path pluginsPath = appLocation.toPath().resolve("Contents/PlugIns");
 327         if (Files.isDirectory(pluginsPath)) {
 328             Files.list(pluginsPath)
 329                     .forEach(signIdentifiedByPList);
 330 
 331             ioe = toThrow.get();
 332             if (ioe != null) {
 333                 throw ioe;
 334             }
 335         }
 336         Path frameworkPath = appLocation.toPath().resolve("Contents/Frameworks");
 337         if (Files.isDirectory(frameworkPath)) {
 338             Files.list(frameworkPath)
 339                     .forEach(signIdentifiedByPList);
 340 
 341             ioe = toThrow.get();
 342             if (ioe != null) {
 343                 throw ioe;
 344             }
 345         }
 346 
 347         // sign the app itself
 348         List<String> args = new ArrayList<>();
 349         args.addAll(Arrays.asList("codesign",
 350                 "-s", signingIdentity, // sign with this key
 351                 "-vvvv")); // super verbose output
 352         if (entitlementsFile != null) {
 353             args.add("--entitlements");
 354             args.add(entitlementsFile); // entitlements
 355         }
 356         if (keyChain != null && !keyChain.isEmpty()) {
 357             args.add("--keychain");
 358             args.add(keyChain);
 359         }
 360         args.add(appLocation.toString());
 361 
 362         ProcessBuilder pb = new ProcessBuilder(args.toArray(new String[args.size()]));
 363         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 364     }
 365 
 366     @Override
 367     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 368         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 369 
 370         results.addAll(MacAppBundler.getAppBundleParameters());
 371         results.addAll(Arrays.asList(
 372                 APP_BUNDLER,
 373                 CONFIG_ROOT,
 374                 APP_IMAGE_BUILD_ROOT,
 375                 MAC_APP_IMAGE
 376         ));
 377 
 378         return results;
 379     }
 380 
 381     @Override
 382     public String getBundleType() {
 383         return "INSTALLER";
 384     }
 385     
 386     public static String findKey(String key, String keychainName, boolean verbose) {
 387 
 388         try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos)) {
 389             List<String> searchOptions = new ArrayList<>();
 390             searchOptions.add("security");
 391             searchOptions.add("find-certificate");
 392             searchOptions.add("-c");
 393             searchOptions.add(key);
 394             searchOptions.add("-a");
 395             if (keychainName != null && !keychainName.isEmpty()) {
 396                 searchOptions.add(keychainName);
 397             }
 398 
 399             ProcessBuilder pb = new ProcessBuilder(searchOptions);
 400 
 401             IOUtils.exec(pb, verbose, false, ps);
 402             Pattern p = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"");
 403             Matcher m = p.matcher(baos.toString());
 404             if (!m.find()) {
 405                 Log.info("Did not find a key matching '" + key + "'");
 406                 return null;
 407             }
 408             String matchedKey = m.group(1);
 409             if (m.find()) {
 410                 Log.info("Found more than one key matching '"  + key + "'");
 411                 return null;
 412             }
 413             Log.debug("Using key '" + matchedKey + "'");
 414             return matchedKey;
 415         } catch (IOException ioe) {
 416             ioe.printStackTrace();
 417             Log.verbose(ioe);
 418             return null;
 419         }
 420     }
 421 }