001/** 002 * Copyright (C) 2006-2020 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.server.tomcat; 017 018import static java.util.Locale.ROOT; 019import static java.util.Optional.ofNullable; 020 021import java.io.ByteArrayOutputStream; 022import java.io.File; 023import java.io.IOException; 024import java.io.InputStream; 025import java.net.InetAddress; 026import java.net.UnknownHostException; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.StringTokenizer; 030import java.util.stream.Stream; 031 032import org.apache.meecrowave.Meecrowave; 033import org.apache.meecrowave.configuration.Configuration; 034import org.eclipse.microprofile.config.Config; 035import org.eclipse.microprofile.config.ConfigProvider; 036 037import lombok.extern.slf4j.Slf4j; 038 039// mainly for compatibility with vault-proxy, when the vault-proxy is dropped we can drop it 040@Slf4j 041public class GenerateCertificateAndActivateHttps implements Meecrowave.ConfigurationCustomizer { 042 043 @Override 044 public void accept(final Configuration builder) { 045 final Config config = ConfigProvider.getConfig(); 046 if (!config.getOptionalValue("talend.component.server.ssl.active", Boolean.class).orElse(false)) { 047 log.debug("Automatic ssl setup is not active, skipping"); 048 return; 049 } 050 051 log.debug("Automatic ssl setup is active"); 052 final String password = 053 config.getOptionalValue("talend.component.server.ssl.password", String.class).orElse("changeit"); 054 final String location = config 055 .getOptionalValue("talend.component.server.ssl.keystore.location", String.class) 056 .orElse(new File(System.getProperty("meecrowave.base", "."), "conf/ssl.p12").getAbsolutePath()); 057 final String alias = 058 config.getOptionalValue("talend.component.server.ssl.keystore.alias", String.class).orElse("talend"); 059 final String keystoreType = 060 config.getOptionalValue("talend.component.server.ssl.keystore.type", String.class).orElse("PKCS12"); 061 final File keystoreLocation = new File(location); 062 if (!keystoreLocation.exists() || config 063 .getOptionalValue("talend.component.server.ssl.keystore.generation.force", Boolean.class) 064 .orElse(false)) { 065 final String generateCommand = config 066 .getOptionalValue("talend.component.server.ssl.keystore.generation.command", String.class) 067 .orElse(null); 068 if (keystoreLocation.getParentFile() == null 069 || (!keystoreLocation.getParentFile().exists() && !keystoreLocation.getParentFile().mkdirs())) { 070 throw new IllegalArgumentException("Can't create '" + keystoreLocation + "'"); 071 } 072 try { 073 if (generateCommand != null) { 074 log.debug("Generating certificate for HTTPS using a custom command"); 075 doExec(parseCommand(generateCommand)); 076 } else { 077 log.debug("Generating certificate for HTTPS"); 078 doExec(new String[] { findKeyTool(), "-genkey", "-keyalg", 079 config 080 .getOptionalValue("talend.component.server.ssl.keypair.algorithm", String.class) 081 .orElse("RSA"), 082 "-alias", alias, "-keystore", keystoreLocation.getAbsolutePath(), "-storepass", password, 083 "-keypass", password, "-noprompt", "-dname", 084 config 085 .getOptionalValue("talend.component.server.ssl.certificate.dname", String.class) 086 .orElseGet( 087 () -> "CN=Talend,OU=www.talend.com,O=component-server,C=" + getLocalId()), 088 "-storetype", keystoreType, "-keysize", 089 config 090 .getOptionalValue("talend.component.server.ssl.keypair.size", Integer.class) 091 .map(String::valueOf) 092 .orElse("2048") }); 093 } 094 } catch (final InterruptedException ie) { 095 log.error(ie.getMessage(), ie); 096 Thread.currentThread().interrupt(); 097 } catch (final Exception e) { 098 log.error(e.getMessage(), e); 099 throw new IllegalStateException(e); 100 } 101 } 102 103 builder.setSkipHttp(true); 104 builder.setSsl(true); 105 builder 106 .setHttpsPort(config 107 .getOptionalValue("talend.component.server.ssl.port", Integer.class) 108 .orElseGet(builder::getHttpPort)); 109 builder.setKeystorePass(password); 110 builder.setKeystoreFile(keystoreLocation.getAbsolutePath()); 111 builder.setKeystoreType(keystoreType); 112 builder.setKeyAlias(alias); 113 log.info("Configured HTTPS using '{}' on port {}", builder.getKeystoreFile(), builder.getHttpsPort()); 114 } 115 116 private String getLocalId() { 117 try { 118 return InetAddress.getLocalHost().getHostName().replace('.', '_') /* just in case of a misconfiguration */; 119 } catch (final UnknownHostException e) { 120 return "local"; 121 } 122 } 123 124 private void doExec(final String[] command) throws InterruptedException, IOException { 125 final Process process = new ProcessBuilder(command).start(); 126 new Thread(new KeyToolStream("stdout", process.getInputStream())).start(); 127 new Thread(new KeyToolStream("stderr", process.getErrorStream())).start(); 128 final int status = process.waitFor(); 129 if (status != 0) { 130 throw new IllegalStateException( 131 "Can't generate the certificate, exist code=" + status + ", check out stdout/stderr for details"); 132 } 133 } 134 135 private String findKeyTool() { 136 final String ext = System.getProperty("os.name", "ignore").toLowerCase(ROOT).contains("win") ? ".exe" : ""; 137 final File javaHome = new File(System.getProperty("java.home", ".")); 138 if (javaHome.exists()) { 139 final File keyTool = new File(javaHome, "bin/keytool" + ext); 140 if (keyTool.exists()) { 141 return keyTool.getAbsolutePath(); 142 } 143 final File jreKeyTool = new File(javaHome, "jre/bin/keytool" + ext); 144 if (jreKeyTool.exists()) { 145 return jreKeyTool.getAbsolutePath(); 146 } 147 } 148 // else check in the path 149 final String path = ofNullable(System.getenv("PATH")) 150 .orElseGet(() -> System 151 .getenv() 152 .keySet() 153 .stream() 154 .filter(it -> it.equalsIgnoreCase("path")) 155 .findFirst() 156 .map(System::getenv) 157 .orElse(null)); 158 if (path != null) { 159 return Stream 160 .of(path.split(File.pathSeparator)) 161 .map(it -> new File(it, "keytool")) 162 .filter(File::exists) 163 .findFirst() 164 .map(File::getAbsolutePath) 165 .orElse(null); 166 } 167 throw new IllegalStateException("Didn't find keytool"); 168 } 169 170 // from ant 171 private static String[] parseCommand(final String cmd) { 172 if (cmd == null || cmd.isEmpty()) { 173 return new String[0]; 174 } 175 176 final int normal = 0; 177 final int inQuote = 1; 178 final int inDoubleQuote = 2; 179 int state = normal; 180 final StringTokenizer tok = new StringTokenizer(cmd, "\"\' ", true); 181 final Collection<String> v = new ArrayList<>(); 182 StringBuffer current = new StringBuffer(); 183 boolean lastTokenHasBeenQuoted = false; 184 185 while (tok.hasMoreTokens()) { 186 String nextTok = tok.nextToken(); 187 switch (state) { 188 case inQuote: 189 if ("\'".equals(nextTok)) { 190 lastTokenHasBeenQuoted = true; 191 state = normal; 192 } else { 193 current.append(nextTok); 194 } 195 break; 196 case inDoubleQuote: 197 if ("\"".equals(nextTok)) { 198 lastTokenHasBeenQuoted = true; 199 state = normal; 200 } else { 201 current.append(nextTok); 202 } 203 break; 204 default: 205 if ("\'".equals(nextTok)) { 206 state = inQuote; 207 } else if ("\"".equals(nextTok)) { 208 state = inDoubleQuote; 209 } else if (" ".equals(nextTok)) { 210 if (lastTokenHasBeenQuoted || current.length() != 0) { 211 v.add(current.toString()); 212 current = new StringBuffer(); 213 } 214 } else { 215 current.append(nextTok); 216 } 217 lastTokenHasBeenQuoted = false; 218 break; 219 } 220 } 221 if (lastTokenHasBeenQuoted || current.length() != 0) { 222 v.add(current.toString()); 223 } 224 if (state == inQuote || state == inDoubleQuote) { 225 throw new RuntimeException("unbalanced quotes in " + cmd); 226 } 227 return v.toArray(new String[0]); 228 } 229 230 @Slf4j 231 private static class KeyToolStream implements Runnable { 232 233 private final String name; 234 235 private final InputStream stream; 236 237 private final ByteArrayOutputStream builder = new ByteArrayOutputStream(); 238 239 private KeyToolStream(final String name, final InputStream stream) { 240 this.name = name; 241 this.stream = stream; 242 } 243 244 @Override 245 public void run() { 246 try { 247 final byte[] buf = new byte[64]; 248 int num; 249 while ((num = stream.read(buf)) != -1) { // todo: rework it to handle EOL 250 for (int i = 0; i < num; i++) { 251 if (buf[i] == '\r' || buf[i] == '\n') { 252 doLog(); 253 builder.reset(); 254 } else { 255 builder.write(buf[i]); 256 } 257 } 258 } 259 if (builder.size() > 0) { 260 doLog(); 261 } 262 } catch (final IOException e) { 263 // no-op 264 } 265 } 266 267 private void doLog() { 268 final String string = builder.toString().trim(); 269 if (string.isEmpty()) { 270 return; 271 } 272 log.info("[" + name + "] " + string); 273 } 274 275 } 276}