001/*
002 * Copyright (C) 2022-2023 The Prometheus jmx_exporter Authors
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 */
016
017package io.prometheus.jmx.common.http;
018
019import com.sun.net.httpserver.Authenticator;
020import com.sun.net.httpserver.HttpsConfigurator;
021import io.prometheus.client.CollectorRegistry;
022import io.prometheus.client.exporter.HTTPServer;
023import io.prometheus.jmx.common.http.authenticator.MessageDigestAuthenticator;
024import io.prometheus.jmx.common.http.authenticator.PBKDF2Authenticator;
025import io.prometheus.jmx.common.http.authenticator.PlaintextAuthenticator;
026import io.prometheus.jmx.common.http.ssl.SSLContextFactory;
027import io.prometheus.jmx.common.yaml.YamlMapAccessor;
028import io.prometheus.jmx.common.configuration.ConvertToInteger;
029import io.prometheus.jmx.common.configuration.ConvertToMapAccessor;
030import io.prometheus.jmx.common.configuration.ConvertToString;
031import io.prometheus.jmx.common.configuration.ValidatStringIsNotBlank;
032import io.prometheus.jmx.common.configuration.ValidateIntegerInRange;
033import org.yaml.snakeyaml.Yaml;
034
035import java.io.File;
036import java.io.FileReader;
037import java.io.IOException;
038import java.io.Reader;
039import java.net.InetSocketAddress;
040import java.security.GeneralSecurityException;
041import java.util.HashMap;
042import java.util.HashSet;
043import java.util.Map;
044import java.util.Set;
045
046/**
047 * Class to create the HTTPServer used by both the Java agent exporter and the standalone exporter
048 */
049public class HTTPServerFactory {
050
051    private static final String REALM = "/";
052    private static final String PLAINTEXT = "plaintext";
053    private static final Set<String> SHA_ALGORITHMS;
054    private static final Set<String> PBKDF2_ALGORITHMS;
055    private static final Map<String, Integer> PBKDF2_ALGORITHM_ITERATIONS;
056    private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore";
057    private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword";
058
059    private static final int PBKDF2_KEY_LENGTH_BITS = 128;
060
061    static {
062        SHA_ALGORITHMS = new HashSet<>();
063        SHA_ALGORITHMS.add("SHA-1");
064        SHA_ALGORITHMS.add("SHA-256");
065        SHA_ALGORITHMS.add("SHA-512");
066
067        PBKDF2_ALGORITHMS = new HashSet<>();
068        PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA1");
069        PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA256");
070        PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA512");
071
072        PBKDF2_ALGORITHM_ITERATIONS = new HashMap<>();
073        PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA1", 1300000);
074        PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA256", 600000);
075        PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA512", 210000);
076    }
077
078    private YamlMapAccessor rootYamlMapAccessor;
079
080    /**
081     * Constructor
082     */
083    public HTTPServerFactory() {
084        // DO NOTHING
085    }
086
087    /**
088     * Method to create an HTTPServer using the supplied arguments
089     *
090     * @param inetSocketAddress inetSocketAddress
091     * @param collectorRegistry collectorRegistry
092     * @param daemon daemon
093     * @param exporterYamlFile  exporterYamlFile
094     * @return an HTTPServer
095     * @throws IOException IOException
096     */
097    public HTTPServer createHTTPServer(
098            InetSocketAddress inetSocketAddress,
099            CollectorRegistry collectorRegistry,
100            boolean daemon,
101            File exporterYamlFile) throws IOException {
102
103        HTTPServer.Builder httpServerBuilder =
104                new HTTPServer.Builder()
105                        .withInetSocketAddress(inetSocketAddress)
106                        .withRegistry(collectorRegistry)
107                        .withDaemonThreads(daemon);
108
109        createMapAccessor(exporterYamlFile);
110        configureAuthentication(httpServerBuilder);
111        configureSSL(httpServerBuilder);
112
113        return httpServerBuilder.build();
114    }
115
116    /**
117     * Method to create a MapAccessor for accessing YAML configuration
118     *
119     * @param exporterYamlFile exporterYamlFile
120     */
121    private void createMapAccessor(File exporterYamlFile) {
122        try (Reader reader = new FileReader(exporterYamlFile)) {
123            Map<Object, Object> yamlMap = new Yaml().load(reader);
124            rootYamlMapAccessor = new YamlMapAccessor(yamlMap);
125        } catch (Throwable t) {
126            throw new ConfigurationException(
127                    String.format(
128                            "Exception loading exporter YAML file [%s]",
129                            exporterYamlFile),
130                    t);
131        }
132    }
133
134    /**
135     * Method to configuration authentication
136     *
137     * @param httpServerBuilder httpServerBuilder
138     */
139    private void configureAuthentication(HTTPServer.Builder httpServerBuilder) {
140        if (rootYamlMapAccessor.containsPath("/httpServer/authentication")) {
141            YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor =
142                    rootYamlMapAccessor
143                            .get("/httpServer/authentication/basic")
144                            .map(new ConvertToMapAccessor(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic")))
145                            .orElseThrow(ConfigurationException.supplier("/httpServer/authentication/basic configuration values are required"));
146
147            String username =
148                    httpServerAuthenticationBasicYamlMapAccessor
149                            .get("/username")
150                            .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/username must be a string")))
151                            .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/username must not be blank")))
152                            .orElseThrow(ConfigurationException.supplier("/httpServer/authentication/basic/username is a required string"));
153
154            String algorithm =
155                    httpServerAuthenticationBasicYamlMapAccessor
156                            .get("/algorithm")
157                            .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/algorithm must be a string")))
158                            .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/algorithm must not be blank")))
159                            .orElse(PLAINTEXT);
160
161            Authenticator authenticator;
162
163            if (PLAINTEXT.equalsIgnoreCase(algorithm)) {
164                String password =
165                        httpServerAuthenticationBasicYamlMapAccessor
166                                .get("/password")
167                                .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/password must be a string")))
168                                .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/password must not be blank")))
169                                .orElseThrow(ConfigurationException.supplier("/httpServer/authentication/basic/password is a required string"));
170
171                authenticator = new PlaintextAuthenticator("/", username, password);
172            } else if (SHA_ALGORITHMS.contains(algorithm) || PBKDF2_ALGORITHMS.contains(algorithm)) {
173                String hash =
174                        httpServerAuthenticationBasicYamlMapAccessor
175                                .get("/passwordHash")
176                                .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/passwordHash must be a string")))
177                                .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/passwordHash must not be blank")))
178                                .orElseThrow(ConfigurationException.supplier("/httpServer/authentication/basic/passwordHash is a required string"));
179
180                if (SHA_ALGORITHMS.contains(algorithm)) {
181                    authenticator = createMessageDigestAuthenticator(
182                            httpServerAuthenticationBasicYamlMapAccessor,
183                            REALM,
184                            username,
185                            hash,
186                            algorithm);
187                } else {
188                    authenticator = createPBKDF2Authenticator(
189                            httpServerAuthenticationBasicYamlMapAccessor,
190                            REALM,
191                            username,
192                            hash,
193                            algorithm);
194                }
195            } else {
196                throw new ConfigurationException(
197                        String.format("Unsupported /httpServer/authentication/basic/algorithm [%s]", algorithm));
198            }
199
200            httpServerBuilder.withAuthenticator(authenticator);
201        }
202    }
203
204    /**
205     * Method to create a MessageDigestAuthenticator
206     *
207     * @param httpServerAuthenticationBasicYamlMapAccessor httpServerAuthenticationBasicMapAccessor
208     * @param realm realm
209     * @param username username
210     * @param password password
211     * @param algorithm algorithm
212     * @return a MessageDigestAuthenticator
213     */
214    private Authenticator createMessageDigestAuthenticator(
215            YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor, String realm, String username, String password, String algorithm) {
216        String salt =
217                httpServerAuthenticationBasicYamlMapAccessor
218                        .get("/salt")
219                        .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/salt must be a string")))
220                        .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/salt must not be blank")))
221                        .orElseThrow(ConfigurationException.supplier("/httpServer/authentication/basic/salt is a required string"));
222
223        try {
224            return new MessageDigestAuthenticator(realm, username, password, algorithm, salt);
225        } catch (GeneralSecurityException e) {
226            throw new ConfigurationException(
227                    String.format(
228                            "Invalid /httpServer/authentication/basic/algorithm, unsupported algorithm [%s]",
229                            algorithm));
230        }
231    }
232
233    /**
234     * Method to create a PBKDF2Authenticator
235     *
236     * @param httpServerAuthenticationBasicYamlMapAccessor httpServerAuthenticationBasicMapAccessor
237     * @param realm realm
238     * @param username username
239     * @param password password
240     * @param algorithm algorithm
241     * @return a PBKDF2Authenticator
242     */
243    private Authenticator createPBKDF2Authenticator(
244            YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor, String realm, String username, String password, String algorithm) {
245        String salt =
246                httpServerAuthenticationBasicYamlMapAccessor
247                        .get("/salt")
248                        .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/salt must be a string")))
249                        .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/salt must be not blank")))
250                        .orElseThrow(ConfigurationException.supplier("/httpServer/authentication/basic/salt is a required string"));
251
252        int iterations =
253                httpServerAuthenticationBasicYamlMapAccessor
254                        .get("/iterations")
255                        .map(new ConvertToInteger(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/iterations must be an integer")))
256                        .map(new ValidateIntegerInRange(1, Integer.MAX_VALUE, ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/iterations must be between greater than 0")))
257                        .orElse(PBKDF2_ALGORITHM_ITERATIONS.get(algorithm));
258
259        int keyLength =
260                httpServerAuthenticationBasicYamlMapAccessor
261                        .get("/keyLength")
262                        .map(new ConvertToInteger(ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/keyLength must be an integer")))
263                        .map(new ValidateIntegerInRange(1, Integer.MAX_VALUE, ConfigurationException.supplier("Invalid configuration for /httpServer/authentication/basic/keyLength must be greater than 0")))
264                        .orElse(PBKDF2_KEY_LENGTH_BITS);
265
266        try {
267            return new PBKDF2Authenticator(realm, username, password, algorithm, salt, iterations, keyLength);
268        } catch (GeneralSecurityException e) {
269            throw new ConfigurationException(
270                    String.format(
271                            "Invalid /httpServer/authentication/basic/algorithm, unsupported algorithm [%s]",
272                            algorithm));
273        }
274    }
275
276    /**
277     * Method to configure SSL
278     *
279     * @param httpServerBuilder httpServerBuilder
280     */
281    public void configureSSL(HTTPServer.Builder httpServerBuilder) {
282        if (rootYamlMapAccessor.containsPath("/httpServer/ssl")) {
283            try {
284                String keyStoreFilename =
285                        rootYamlMapAccessor
286                                .get("/httpServer/ssl/keyStore/filename")
287                                .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/ssl/keyStore/filename must be a string")))
288                                .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/ssl/keyStore/filename must not be blank")))
289                                .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE));
290
291                String keyStorePassword =
292                        rootYamlMapAccessor
293                                .get("/httpServer/ssl/keyStore/password")
294                                .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/ssl/keyStore/password must be a string")))
295                                .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/ssl/keyStore/password must not be blank")))
296                                .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD));
297
298                String certificateAlias =
299                        rootYamlMapAccessor
300                                .get("/httpServer/ssl/certificate/alias")
301                                .map(new ConvertToString(ConfigurationException.supplier("Invalid configuration for /httpServer/ssl/certificate/alias must be a string")))
302                                .map(new ValidatStringIsNotBlank(ConfigurationException.supplier("Invalid configuration for /httpServer/ssl/certificate/alias must not be blank")))
303                                .orElseThrow(ConfigurationException.supplier("/httpServer/ssl/certificate/alias is a required string"));
304
305                httpServerBuilder.withHttpsConfigurator(
306                        new HttpsConfigurator(SSLContextFactory.createSSLContext(
307                                keyStoreFilename,
308                                keyStorePassword,
309                                certificateAlias)));
310            } catch (GeneralSecurityException | IOException e) {
311                String message = e.getMessage();
312                if (message != null && !message.trim().isEmpty()) {
313                    message = ", " + message.trim();
314                } else {
315                    message = "";
316                }
317
318                throw new ConfigurationException(
319                        String.format("Exception loading SSL configuration%s", message), e);
320            }
321        }
322    }
323}