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}