001/*
002 * Copyright (C) 2022-present 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.jmx.common.configuration.ConvertToInteger;
022import io.prometheus.jmx.common.configuration.ConvertToMapAccessor;
023import io.prometheus.jmx.common.configuration.ConvertToString;
024import io.prometheus.jmx.common.configuration.ValidateIntegerInRange;
025import io.prometheus.jmx.common.configuration.ValidateStringIsNotBlank;
026import io.prometheus.jmx.common.http.authenticator.MessageDigestAuthenticator;
027import io.prometheus.jmx.common.http.authenticator.PBKDF2Authenticator;
028import io.prometheus.jmx.common.http.authenticator.PlaintextAuthenticator;
029import io.prometheus.jmx.common.http.ssl.SSLContextFactory;
030import io.prometheus.jmx.common.yaml.YamlMapAccessor;
031import io.prometheus.metrics.exporter.httpserver.HTTPServer;
032import io.prometheus.metrics.model.registry.PrometheusRegistry;
033import java.io.File;
034import java.io.FileReader;
035import java.io.IOException;
036import java.io.Reader;
037import java.net.InetAddress;
038import java.security.GeneralSecurityException;
039import java.util.HashMap;
040import java.util.HashSet;
041import java.util.Map;
042import java.util.Optional;
043import java.util.Set;
044import java.util.concurrent.Executors;
045import java.util.concurrent.RejectedExecutionHandler;
046import java.util.concurrent.SynchronousQueue;
047import java.util.concurrent.ThreadFactory;
048import java.util.concurrent.ThreadPoolExecutor;
049import java.util.concurrent.TimeUnit;
050import java.util.concurrent.atomic.AtomicInteger;
051import org.yaml.snakeyaml.Yaml;
052
053/**
054 * Class to create the HTTPServer used by both the Java agent exporter and the standalone exporter
055 */
056public class HTTPServerFactory {
057
058    private static final int DEFAULT_MINIMUM_THREADS = 1;
059    private static final int DEFAULT_MAXIMUM_THREADS = 10;
060    private static final int DEFAULT_KEEP_ALIVE_TIME_SECONDS = 120;
061
062    private static final String REALM = "/";
063    private static final String PLAINTEXT = "plaintext";
064    private static final Set<String> SHA_ALGORITHMS;
065    private static final Set<String> PBKDF2_ALGORITHMS;
066    private static final Map<String, Integer> PBKDF2_ALGORITHM_ITERATIONS;
067    private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore";
068    private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword";
069
070    private static final int PBKDF2_KEY_LENGTH_BITS = 128;
071
072    static {
073        SHA_ALGORITHMS = new HashSet<>();
074        SHA_ALGORITHMS.add("SHA-1");
075        SHA_ALGORITHMS.add("SHA-256");
076        SHA_ALGORITHMS.add("SHA-512");
077
078        PBKDF2_ALGORITHMS = new HashSet<>();
079        PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA1");
080        PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA256");
081        PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA512");
082
083        PBKDF2_ALGORITHM_ITERATIONS = new HashMap<>();
084        PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA1", 1300000);
085        PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA256", 600000);
086        PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA512", 210000);
087    }
088
089    private YamlMapAccessor rootYamlMapAccessor;
090
091    /** Constructor */
092    public HTTPServerFactory() {
093        // DO NOTHING
094    }
095
096    /**
097     * Method to create an HTTPServer using the supplied arguments
098     *
099     * @param inetAddress inetAddress
100     * @param port port
101     * @param prometheusRegistry prometheusRegistry
102     * @param exporterYamlFile exporterYamlFile
103     * @return an HTTPServer
104     * @throws IOException IOException
105     */
106    public HTTPServer createHTTPServer(
107            InetAddress inetAddress,
108            int port,
109            PrometheusRegistry prometheusRegistry,
110            File exporterYamlFile)
111            throws IOException {
112        HTTPServer.Builder httpServerBuilder =
113                HTTPServer.builder()
114                        .inetAddress(inetAddress)
115                        .port(port)
116                        .registry(prometheusRegistry);
117
118        createMapAccessor(exporterYamlFile);
119        configureThreads(httpServerBuilder);
120        configureAuthentication(httpServerBuilder);
121        configureSSL(httpServerBuilder);
122
123        return httpServerBuilder.buildAndStart();
124    }
125
126    /**
127     * Method to create a MapAccessor for accessing YAML configuration
128     *
129     * @param exporterYamlFile exporterYamlFile
130     */
131    private void createMapAccessor(File exporterYamlFile) {
132        try (Reader reader = new FileReader(exporterYamlFile)) {
133            Map<Object, Object> yamlMap = new Yaml().load(reader);
134            rootYamlMapAccessor = new YamlMapAccessor(yamlMap);
135        } catch (Throwable t) {
136            throw new ConfigurationException(
137                    String.format("Exception loading exporter YAML file [%s]", exporterYamlFile),
138                    t);
139        }
140    }
141
142    /**
143     * Method to configure the HTTPServer thread pool
144     *
145     * @param httpServerBuilder httpServerBuilder
146     */
147    private void configureThreads(HTTPServer.Builder httpServerBuilder) {
148        int minimum = DEFAULT_MINIMUM_THREADS;
149        int maximum = DEFAULT_MAXIMUM_THREADS;
150        int keepAliveTime = DEFAULT_KEEP_ALIVE_TIME_SECONDS;
151
152        if (rootYamlMapAccessor.containsPath("/httpServer/threads")) {
153            YamlMapAccessor httpServerThreadsMapAccessor =
154                    rootYamlMapAccessor
155                            .get("/httpServer/threads")
156                            .map(
157                                    new ConvertToMapAccessor(
158                                            ConfigurationException.supplier(
159                                                    "Invalid configuration for"
160                                                            + " /httpServer/threads")))
161                            .orElseThrow(
162                                    ConfigurationException.supplier(
163                                            "/httpServer/threads configuration values are"
164                                                    + " required"));
165
166            minimum =
167                    httpServerThreadsMapAccessor
168                            .get("/minimum")
169                            .map(
170                                    new ConvertToInteger(
171                                            ConfigurationException.supplier(
172                                                    "Invalid configuration for"
173                                                        + " /httpServer/threads/minimum must be an"
174                                                        + " integer")))
175                            .map(
176                                    new ValidateIntegerInRange(
177                                            0,
178                                            Integer.MAX_VALUE,
179                                            ConfigurationException.supplier(
180                                                    "Invalid configuration for"
181                                                        + " /httpServer/threads/minimum must be 0"
182                                                        + " or greater")))
183                            .orElseThrow(
184                                    ConfigurationException.supplier(
185                                            "/httpServer/threads/minimum is a required integer"));
186
187            maximum =
188                    httpServerThreadsMapAccessor
189                            .get("/maximum")
190                            .map(
191                                    new ConvertToInteger(
192                                            ConfigurationException.supplier(
193                                                    "Invalid configuration for"
194                                                        + " /httpServer/threads/maximum must be an"
195                                                        + " integer")))
196                            .map(
197                                    new ValidateIntegerInRange(
198                                            1,
199                                            Integer.MAX_VALUE,
200                                            ConfigurationException.supplier(
201                                                    "Invalid configuration for"
202                                                        + " /httpServer/threads/maxPoolSize must be"
203                                                        + " between greater than 0")))
204                            .orElseThrow(
205                                    ConfigurationException.supplier(
206                                            "/httpServer/threads/maximum is a required integer"));
207
208            keepAliveTime =
209                    httpServerThreadsMapAccessor
210                            .get("/keepAliveTime")
211                            .map(
212                                    new ConvertToInteger(
213                                            ConfigurationException.supplier(
214                                                    "Invalid configuration for"
215                                                        + " /httpServer/threads/keepAliveTime must"
216                                                        + " be an integer")))
217                            .map(
218                                    new ValidateIntegerInRange(
219                                            1,
220                                            Integer.MAX_VALUE,
221                                            ConfigurationException.supplier(
222                                                    "Invalid configuration for"
223                                                        + " /httpServer/threads/keepAliveTime must"
224                                                        + " be greater than 0")))
225                            .orElseThrow(
226                                    ConfigurationException.supplier(
227                                            "/httpServer/threads/keepAliveTime is a required"
228                                                    + " integer"));
229
230            if (maximum < minimum) {
231                throw new ConfigurationException(
232                        "/httpServer/threads/maximum must be greater than or equal to"
233                                + " /httpServer/threads/minimum");
234            }
235        }
236
237        ThreadPoolExecutor threadPoolExecutor =
238                new ThreadPoolExecutor(
239                        minimum,
240                        maximum,
241                        keepAliveTime,
242                        TimeUnit.SECONDS,
243                        new SynchronousQueue<>(true),
244                        NamedDaemonThreadFactory.defaultThreadFactory(true),
245                        new BlockingRejectedExecutionHandler());
246
247        httpServerBuilder.executorService(threadPoolExecutor);
248    }
249
250    /**
251     * Method to configure authentication
252     *
253     * @param httpServerBuilder httpServerBuilder
254     */
255    private void configureAuthentication(HTTPServer.Builder httpServerBuilder) {
256        Authenticator authenticator;
257
258        if (rootYamlMapAccessor.containsPath("/httpServer/authentication")) {
259            Optional<Object> authenticatorClassAttribute =
260                    rootYamlMapAccessor.get("/httpServer/authentication/plugin");
261            if (authenticatorClassAttribute.isPresent()) {
262
263                YamlMapAccessor httpServerAuthenticationCustomAuthenticatorYamlMapAccessor =
264                        rootYamlMapAccessor
265                                .get("/httpServer/authentication/plugin")
266                                .map(
267                                        new ConvertToMapAccessor(
268                                                ConfigurationException.supplier(
269                                                        "Invalid configuration for"
270                                                            + " /httpServer/authentication/plugin")))
271                                .orElseThrow(
272                                        ConfigurationException.supplier(
273                                                "/httpServer/authentication/plugin"
274                                                        + " configuration values are required"));
275
276                String authenticatorClass =
277                        httpServerAuthenticationCustomAuthenticatorYamlMapAccessor
278                                .get("/class")
279                                .map(
280                                        new ConvertToString(
281                                                ConfigurationException.supplier(
282                                                        "Invalid configuration for"
283                                                            + " /httpServer/authentication/plugin/class"
284                                                            + " must be a string")))
285                                .map(
286                                        new ValidateStringIsNotBlank(
287                                                ConfigurationException.supplier(
288                                                        "Invalid configuration for"
289                                                            + " /httpServer/authentication/plugin/class"
290                                                            + " must not be blank")))
291                                .orElseThrow(
292                                        ConfigurationException.supplier(
293                                                "/httpServer/authentication/plugin/class is a"
294                                                        + " required string"));
295
296                Optional<Object> subjectAttribute =
297                        httpServerAuthenticationCustomAuthenticatorYamlMapAccessor.get(
298                                "/subjectAttributeName");
299                if (subjectAttribute.isPresent()) {
300                    String subjectAttributeName =
301                            subjectAttribute
302                                    .map(
303                                            new ConvertToString(
304                                                    ConfigurationException.supplier(
305                                                            "Invalid configuration for"
306                                                                + " /httpServer/authentication/plugin/class/subjectAttributeName"
307                                                                + " must be a string")))
308                                    .map(
309                                            new ValidateStringIsNotBlank(
310                                                    ConfigurationException.supplier(
311                                                            "Invalid configuration for"
312                                                                + " /httpServer/authentication/plugin/subjectAttributeName"
313                                                                + " must not be blank")))
314                                    .get();
315
316                    // need subject.doAs for subsequent handlers
317                    httpServerBuilder.authenticatedSubjectAttributeName(subjectAttributeName);
318                }
319
320                authenticator = loadAuthenticator(authenticatorClass);
321            } else {
322                YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor =
323                        rootYamlMapAccessor
324                                .get("/httpServer/authentication/basic")
325                                .map(
326                                        new ConvertToMapAccessor(
327                                                ConfigurationException.supplier(
328                                                        "Invalid configuration for"
329                                                            + " /httpServer/authentication/basic")))
330                                .orElseThrow(
331                                        ConfigurationException.supplier(
332                                                "/httpServer/authentication/basic configuration"
333                                                        + " values are required"));
334
335                String username =
336                        httpServerAuthenticationBasicYamlMapAccessor
337                                .get("/username")
338                                .map(
339                                        new ConvertToString(
340                                                ConfigurationException.supplier(
341                                                        "Invalid configuration for"
342                                                            + " /httpServer/authentication/basic/username"
343                                                            + " must be a string")))
344                                .map(
345                                        new ValidateStringIsNotBlank(
346                                                ConfigurationException.supplier(
347                                                        "Invalid configuration for"
348                                                            + " /httpServer/authentication/basic/username"
349                                                            + " must not be blank")))
350                                .orElseThrow(
351                                        ConfigurationException.supplier(
352                                                "/httpServer/authentication/basic/username is a"
353                                                        + " required string"));
354
355                String algorithm =
356                        httpServerAuthenticationBasicYamlMapAccessor
357                                .get("/algorithm")
358                                .map(
359                                        new ConvertToString(
360                                                ConfigurationException.supplier(
361                                                        "Invalid configuration for"
362                                                            + " /httpServer/authentication/basic/algorithm"
363                                                            + " must be a string")))
364                                .map(
365                                        new ValidateStringIsNotBlank(
366                                                ConfigurationException.supplier(
367                                                        "Invalid configuration for"
368                                                            + " /httpServer/authentication/basic/algorithm"
369                                                            + " must not be blank")))
370                                .orElse(PLAINTEXT);
371
372                if (PLAINTEXT.equalsIgnoreCase(algorithm)) {
373                    String password =
374                            httpServerAuthenticationBasicYamlMapAccessor
375                                    .get("/password")
376                                    .map(
377                                            new ConvertToString(
378                                                    ConfigurationException.supplier(
379                                                            "Invalid configuration for"
380                                                                + " /httpServer/authentication/basic/password"
381                                                                + " must be a string")))
382                                    .map(
383                                            new ValidateStringIsNotBlank(
384                                                    ConfigurationException.supplier(
385                                                            "Invalid configuration for"
386                                                                + " /httpServer/authentication/basic/password"
387                                                                + " must not be blank")))
388                                    .orElseThrow(
389                                            ConfigurationException.supplier(
390                                                    "/httpServer/authentication/basic/password is a"
391                                                            + " required string"));
392
393                    authenticator = new PlaintextAuthenticator("/", username, password);
394                } else if (SHA_ALGORITHMS.contains(algorithm)
395                        || PBKDF2_ALGORITHMS.contains(algorithm)) {
396                    String hash =
397                            httpServerAuthenticationBasicYamlMapAccessor
398                                    .get("/passwordHash")
399                                    .map(
400                                            new ConvertToString(
401                                                    ConfigurationException.supplier(
402                                                            "Invalid configuration for"
403                                                                + " /httpServer/authentication/basic/passwordHash"
404                                                                + " must be a string")))
405                                    .map(
406                                            new ValidateStringIsNotBlank(
407                                                    ConfigurationException.supplier(
408                                                            "Invalid configuration for"
409                                                                + " /httpServer/authentication/basic/passwordHash"
410                                                                + " must not be blank")))
411                                    .orElseThrow(
412                                            ConfigurationException.supplier(
413                                                    "/httpServer/authentication/basic/passwordHash"
414                                                            + " is a required string"));
415
416                    if (SHA_ALGORITHMS.contains(algorithm)) {
417                        authenticator =
418                                createMessageDigestAuthenticator(
419                                        httpServerAuthenticationBasicYamlMapAccessor,
420                                        REALM,
421                                        username,
422                                        hash,
423                                        algorithm);
424                    } else {
425                        authenticator =
426                                createPBKDF2Authenticator(
427                                        httpServerAuthenticationBasicYamlMapAccessor,
428                                        REALM,
429                                        username,
430                                        hash,
431                                        algorithm);
432                    }
433                } else {
434                    throw new ConfigurationException(
435                            String.format(
436                                    "Unsupported /httpServer/authentication/basic/algorithm [%s]",
437                                    algorithm));
438                }
439            }
440
441            httpServerBuilder.authenticator(authenticator);
442        }
443    }
444
445    private Authenticator loadAuthenticator(String className) {
446
447        Class<?> clazz;
448        try {
449            clazz = this.getClass().getClassLoader().loadClass(className);
450        } catch (ClassNotFoundException e) {
451            throw new ConfigurationException(
452                    String.format(
453                            "configured /httpServer/authentication/authenticatorClass [%s]"
454                                    + " not found, loadClass resulted in [%s:%s]",
455                            className, e.getClass(), e.getMessage()));
456        }
457        if (!Authenticator.class.isAssignableFrom(clazz)) {
458            throw new ConfigurationException(
459                    String.format(
460                            "configured /httpServer/authentication/authenticatorClass [%s]"
461                                + " loadClass resulted in [%s] of the wrong type, is not assignable"
462                                + " from Authenticator",
463                            className, clazz.getCanonicalName()));
464        }
465        try {
466            return (Authenticator) clazz.getDeclaredConstructor().newInstance();
467        } catch (Exception e) {
468            throw new ConfigurationException(
469                    String.format(
470                            "configured /httpServer/authentication/authenticatorClass [%s] no arg"
471                                    + " constructor newInstance resulted in exception [%s:%s]",
472                            className, e.getClass(), e.getMessage()));
473        }
474    }
475
476    /**
477     * Method to create a MessageDigestAuthenticator
478     *
479     * @param httpServerAuthenticationBasicYamlMapAccessor httpServerAuthenticationBasicMapAccessor
480     * @param realm realm
481     * @param username username
482     * @param password password
483     * @param algorithm algorithm
484     * @return a MessageDigestAuthenticator
485     */
486    private Authenticator createMessageDigestAuthenticator(
487            YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor,
488            String realm,
489            String username,
490            String password,
491            String algorithm) {
492        String salt =
493                httpServerAuthenticationBasicYamlMapAccessor
494                        .get("/salt")
495                        .map(
496                                new ConvertToString(
497                                        ConfigurationException.supplier(
498                                                "Invalid configuration for"
499                                                    + " /httpServer/authentication/basic/salt must"
500                                                    + " be a string")))
501                        .map(
502                                new ValidateStringIsNotBlank(
503                                        ConfigurationException.supplier(
504                                                "Invalid configuration for"
505                                                    + " /httpServer/authentication/basic/salt must"
506                                                    + " not be blank")))
507                        .orElseThrow(
508                                ConfigurationException.supplier(
509                                        "/httpServer/authentication/basic/salt is a required"
510                                                + " string"));
511
512        try {
513            return new MessageDigestAuthenticator(realm, username, password, algorithm, salt);
514        } catch (GeneralSecurityException e) {
515            throw new ConfigurationException(
516                    String.format(
517                            "Invalid /httpServer/authentication/basic/algorithm, unsupported"
518                                    + " algorithm [%s]",
519                            algorithm));
520        }
521    }
522
523    /**
524     * Method to create a PBKDF2Authenticator
525     *
526     * @param httpServerAuthenticationBasicYamlMapAccessor httpServerAuthenticationBasicMapAccessor
527     * @param realm realm
528     * @param username username
529     * @param password password
530     * @param algorithm algorithm
531     * @return a PBKDF2Authenticator
532     */
533    private Authenticator createPBKDF2Authenticator(
534            YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor,
535            String realm,
536            String username,
537            String password,
538            String algorithm) {
539        String salt =
540                httpServerAuthenticationBasicYamlMapAccessor
541                        .get("/salt")
542                        .map(
543                                new ConvertToString(
544                                        ConfigurationException.supplier(
545                                                "Invalid configuration for"
546                                                    + " /httpServer/authentication/basic/salt must"
547                                                    + " be a string")))
548                        .map(
549                                new ValidateStringIsNotBlank(
550                                        ConfigurationException.supplier(
551                                                "Invalid configuration for"
552                                                    + " /httpServer/authentication/basic/salt must"
553                                                    + " be not blank")))
554                        .orElseThrow(
555                                ConfigurationException.supplier(
556                                        "/httpServer/authentication/basic/salt is a required"
557                                                + " string"));
558
559        int iterations =
560                httpServerAuthenticationBasicYamlMapAccessor
561                        .get("/iterations")
562                        .map(
563                                new ConvertToInteger(
564                                        ConfigurationException.supplier(
565                                                "Invalid configuration for"
566                                                    + " /httpServer/authentication/basic/iterations"
567                                                    + " must be an integer")))
568                        .map(
569                                new ValidateIntegerInRange(
570                                        1,
571                                        Integer.MAX_VALUE,
572                                        ConfigurationException.supplier(
573                                                "Invalid configuration for"
574                                                    + " /httpServer/authentication/basic/iterations"
575                                                    + " must be between greater than 0")))
576                        .orElse(PBKDF2_ALGORITHM_ITERATIONS.get(algorithm));
577
578        int keyLength =
579                httpServerAuthenticationBasicYamlMapAccessor
580                        .get("/keyLength")
581                        .map(
582                                new ConvertToInteger(
583                                        ConfigurationException.supplier(
584                                                "Invalid configuration for"
585                                                    + " /httpServer/authentication/basic/keyLength"
586                                                    + " must be an integer")))
587                        .map(
588                                new ValidateIntegerInRange(
589                                        1,
590                                        Integer.MAX_VALUE,
591                                        ConfigurationException.supplier(
592                                                "Invalid configuration for"
593                                                    + " /httpServer/authentication/basic/keyLength"
594                                                    + " must be greater than 0")))
595                        .orElse(PBKDF2_KEY_LENGTH_BITS);
596
597        try {
598            return new PBKDF2Authenticator(
599                    realm, username, password, algorithm, salt, iterations, keyLength);
600        } catch (GeneralSecurityException e) {
601            throw new ConfigurationException(
602                    String.format(
603                            "Invalid /httpServer/authentication/basic/algorithm, unsupported"
604                                    + " algorithm [%s]",
605                            algorithm));
606        }
607    }
608
609    /**
610     * Method to configure SSL
611     *
612     * @param httpServerBuilder httpServerBuilder
613     */
614    public void configureSSL(HTTPServer.Builder httpServerBuilder) {
615        if (rootYamlMapAccessor.containsPath("/httpServer/ssl")) {
616            try {
617                String keyStoreFilename =
618                        rootYamlMapAccessor
619                                .get("/httpServer/ssl/keyStore/filename")
620                                .map(
621                                        new ConvertToString(
622                                                ConfigurationException.supplier(
623                                                        "Invalid configuration for"
624                                                            + " /httpServer/ssl/keyStore/filename"
625                                                            + " must be a string")))
626                                .map(
627                                        new ValidateStringIsNotBlank(
628                                                ConfigurationException.supplier(
629                                                        "Invalid configuration for"
630                                                            + " /httpServer/ssl/keyStore/filename"
631                                                            + " must not be blank")))
632                                .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE));
633
634                String keyStorePassword =
635                        rootYamlMapAccessor
636                                .get("/httpServer/ssl/keyStore/password")
637                                .map(
638                                        new ConvertToString(
639                                                ConfigurationException.supplier(
640                                                        "Invalid configuration for"
641                                                            + " /httpServer/ssl/keyStore/password"
642                                                            + " must be a string")))
643                                .map(
644                                        new ValidateStringIsNotBlank(
645                                                ConfigurationException.supplier(
646                                                        "Invalid configuration for"
647                                                            + " /httpServer/ssl/keyStore/password"
648                                                            + " must not be blank")))
649                                .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD));
650
651                String certificateAlias =
652                        rootYamlMapAccessor
653                                .get("/httpServer/ssl/certificate/alias")
654                                .map(
655                                        new ConvertToString(
656                                                ConfigurationException.supplier(
657                                                        "Invalid configuration for"
658                                                            + " /httpServer/ssl/certificate/alias"
659                                                            + " must be a string")))
660                                .map(
661                                        new ValidateStringIsNotBlank(
662                                                ConfigurationException.supplier(
663                                                        "Invalid configuration for"
664                                                            + " /httpServer/ssl/certificate/alias"
665                                                            + " must not be blank")))
666                                .orElseThrow(
667                                        ConfigurationException.supplier(
668                                                "/httpServer/ssl/certificate/alias is a required"
669                                                        + " string"));
670
671                httpServerBuilder.httpsConfigurator(
672                        new HttpsConfigurator(
673                                SSLContextFactory.createSSLContext(
674                                        keyStoreFilename, keyStorePassword, certificateAlias)));
675            } catch (GeneralSecurityException | IOException e) {
676                String message = e.getMessage();
677                if (message != null && !message.trim().isEmpty()) {
678                    message = ", " + message.trim();
679                } else {
680                    message = "";
681                }
682
683                throw new ConfigurationException(
684                        String.format("Exception loading SSL configuration%s", message), e);
685            }
686        }
687    }
688
689    /**
690     * Class to implement a named thread factory
691     *
692     * <p>Copied from the `prometheus/client_java` `HTTPServer` due to scoping issues / dependencies
693     */
694    private static class NamedDaemonThreadFactory implements ThreadFactory {
695
696        private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
697
698        private final int poolNumber = POOL_NUMBER.getAndIncrement();
699        private final AtomicInteger threadNumber = new AtomicInteger(1);
700        private final ThreadFactory delegate;
701        private final boolean daemon;
702
703        NamedDaemonThreadFactory(ThreadFactory delegate, boolean daemon) {
704            this.delegate = delegate;
705            this.daemon = daemon;
706        }
707
708        @Override
709        public Thread newThread(Runnable r) {
710            Thread t = delegate.newThread(r);
711            t.setName(
712                    String.format(
713                            "prometheus-http-%d-%d", poolNumber, threadNumber.getAndIncrement()));
714            t.setDaemon(daemon);
715            return t;
716        }
717
718        static ThreadFactory defaultThreadFactory(boolean daemon) {
719            return new NamedDaemonThreadFactory(Executors.defaultThreadFactory(), daemon);
720        }
721    }
722
723    /** Class to implement a blocking RejectedExecutionHandler */
724    private static class BlockingRejectedExecutionHandler implements RejectedExecutionHandler {
725
726        @Override
727        public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
728            if (!threadPoolExecutor.isShutdown()) {
729                try {
730                    threadPoolExecutor.getQueue().put(runnable);
731                } catch (InterruptedException e) {
732                    // DO NOTHING
733                }
734            }
735        }
736    }
737}