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}