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