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.configuration.ConvertToInteger; 024import io.prometheus.jmx.common.configuration.ConvertToMapAccessor; 025import io.prometheus.jmx.common.configuration.ConvertToString; 026import io.prometheus.jmx.common.configuration.ValidateIntegerInRange; 027import io.prometheus.jmx.common.configuration.ValidateStringIsNotBlank; 028import io.prometheus.jmx.common.http.authenticator.MessageDigestAuthenticator; 029import io.prometheus.jmx.common.http.authenticator.PBKDF2Authenticator; 030import io.prometheus.jmx.common.http.authenticator.PlaintextAuthenticator; 031import io.prometheus.jmx.common.http.ssl.SSLContextFactory; 032import io.prometheus.jmx.common.yaml.YamlMapAccessor; 033import java.io.File; 034import java.io.FileReader; 035import java.io.IOException; 036import java.io.Reader; 037import java.net.InetSocketAddress; 038import java.security.GeneralSecurityException; 039import java.util.HashMap; 040import java.util.HashSet; 041import java.util.Map; 042import java.util.Set; 043import java.util.concurrent.Executors; 044import java.util.concurrent.RejectedExecutionHandler; 045import java.util.concurrent.SynchronousQueue; 046import java.util.concurrent.ThreadFactory; 047import java.util.concurrent.ThreadPoolExecutor; 048import java.util.concurrent.TimeUnit; 049import java.util.concurrent.atomic.AtomicInteger; 050import org.yaml.snakeyaml.Yaml; 051 052/** 053 * Class to create the HTTPServer used by both the Java agent exporter and the standalone exporter 054 */ 055public class HTTPServerFactory { 056 057 private static final int DEFAULT_MINIMUM_THREADS = 1; 058 private static final int DEFAULT_MAXIMUM_THREADS = 10; 059 private static final int DEFAULT_KEEP_ALIVE_TIME_SECONDS = 120; 060 061 private static final String REALM = "/"; 062 private static final String PLAINTEXT = "plaintext"; 063 private static final Set<String> SHA_ALGORITHMS; 064 private static final Set<String> PBKDF2_ALGORITHMS; 065 private static final Map<String, Integer> PBKDF2_ALGORITHM_ITERATIONS; 066 private static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore"; 067 private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword"; 068 069 private static final int PBKDF2_KEY_LENGTH_BITS = 128; 070 071 static { 072 SHA_ALGORITHMS = new HashSet<>(); 073 SHA_ALGORITHMS.add("SHA-1"); 074 SHA_ALGORITHMS.add("SHA-256"); 075 SHA_ALGORITHMS.add("SHA-512"); 076 077 PBKDF2_ALGORITHMS = new HashSet<>(); 078 PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA1"); 079 PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA256"); 080 PBKDF2_ALGORITHMS.add("PBKDF2WithHmacSHA512"); 081 082 PBKDF2_ALGORITHM_ITERATIONS = new HashMap<>(); 083 PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA1", 1300000); 084 PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA256", 600000); 085 PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA512", 210000); 086 } 087 088 private YamlMapAccessor rootYamlMapAccessor; 089 090 /** Constructor */ 091 public HTTPServerFactory() { 092 // DO NOTHING 093 } 094 095 /** 096 * Method to create an HTTPServer using the supplied arguments 097 * 098 * @param inetSocketAddress inetSocketAddress 099 * @param collectorRegistry collectorRegistry 100 * @param daemon daemon 101 * @param exporterYamlFile exporterYamlFile 102 * @return an HTTPServer 103 * @throws IOException IOException 104 */ 105 public HTTPServer createHTTPServer( 106 InetSocketAddress inetSocketAddress, 107 CollectorRegistry collectorRegistry, 108 boolean daemon, 109 File exporterYamlFile) 110 throws IOException { 111 112 HTTPServer.Builder httpServerBuilder = 113 new HTTPServer.Builder() 114 .withInetSocketAddress(inetSocketAddress) 115 .withRegistry(collectorRegistry) 116 .withDaemonThreads(daemon); 117 118 createMapAccessor(exporterYamlFile); 119 configureThreads(httpServerBuilder); 120 configureAuthentication(httpServerBuilder); 121 configureSSL(httpServerBuilder); 122 123 return httpServerBuilder.build(); 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.withExecutorService(threadPoolExecutor); 248 } 249 250 /** 251 * Method to configure authentication 252 * 253 * @param httpServerBuilder httpServerBuilder 254 */ 255 private void configureAuthentication(HTTPServer.Builder httpServerBuilder) { 256 if (rootYamlMapAccessor.containsPath("/httpServer/authentication")) { 257 YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor = 258 rootYamlMapAccessor 259 .get("/httpServer/authentication/basic") 260 .map( 261 new ConvertToMapAccessor( 262 ConfigurationException.supplier( 263 "Invalid configuration for" 264 + " /httpServer/authentication/basic"))) 265 .orElseThrow( 266 ConfigurationException.supplier( 267 "/httpServer/authentication/basic configuration values" 268 + " are required")); 269 270 String username = 271 httpServerAuthenticationBasicYamlMapAccessor 272 .get("/username") 273 .map( 274 new ConvertToString( 275 ConfigurationException.supplier( 276 "Invalid configuration for" 277 + " /httpServer/authentication/basic/username" 278 + " must be a string"))) 279 .map( 280 new ValidateStringIsNotBlank( 281 ConfigurationException.supplier( 282 "Invalid configuration for" 283 + " /httpServer/authentication/basic/username" 284 + " must not be blank"))) 285 .orElseThrow( 286 ConfigurationException.supplier( 287 "/httpServer/authentication/basic/username is a" 288 + " required string")); 289 290 String algorithm = 291 httpServerAuthenticationBasicYamlMapAccessor 292 .get("/algorithm") 293 .map( 294 new ConvertToString( 295 ConfigurationException.supplier( 296 "Invalid configuration for" 297 + " /httpServer/authentication/basic/algorithm" 298 + " must be a string"))) 299 .map( 300 new ValidateStringIsNotBlank( 301 ConfigurationException.supplier( 302 "Invalid configuration for" 303 + " /httpServer/authentication/basic/algorithm" 304 + " must not be blank"))) 305 .orElse(PLAINTEXT); 306 307 Authenticator authenticator; 308 309 if (PLAINTEXT.equalsIgnoreCase(algorithm)) { 310 String password = 311 httpServerAuthenticationBasicYamlMapAccessor 312 .get("/password") 313 .map( 314 new ConvertToString( 315 ConfigurationException.supplier( 316 "Invalid configuration for" 317 + " /httpServer/authentication/basic/password" 318 + " must be a string"))) 319 .map( 320 new ValidateStringIsNotBlank( 321 ConfigurationException.supplier( 322 "Invalid configuration for" 323 + " /httpServer/authentication/basic/password" 324 + " must not be blank"))) 325 .orElseThrow( 326 ConfigurationException.supplier( 327 "/httpServer/authentication/basic/password is a" 328 + " required string")); 329 330 authenticator = new PlaintextAuthenticator("/", username, password); 331 } else if (SHA_ALGORITHMS.contains(algorithm) 332 || PBKDF2_ALGORITHMS.contains(algorithm)) { 333 String hash = 334 httpServerAuthenticationBasicYamlMapAccessor 335 .get("/passwordHash") 336 .map( 337 new ConvertToString( 338 ConfigurationException.supplier( 339 "Invalid configuration for" 340 + " /httpServer/authentication/basic/passwordHash" 341 + " must be a string"))) 342 .map( 343 new ValidateStringIsNotBlank( 344 ConfigurationException.supplier( 345 "Invalid configuration for" 346 + " /httpServer/authentication/basic/passwordHash" 347 + " must not be blank"))) 348 .orElseThrow( 349 ConfigurationException.supplier( 350 "/httpServer/authentication/basic/passwordHash is a" 351 + " required string")); 352 353 if (SHA_ALGORITHMS.contains(algorithm)) { 354 authenticator = 355 createMessageDigestAuthenticator( 356 httpServerAuthenticationBasicYamlMapAccessor, 357 REALM, 358 username, 359 hash, 360 algorithm); 361 } else { 362 authenticator = 363 createPBKDF2Authenticator( 364 httpServerAuthenticationBasicYamlMapAccessor, 365 REALM, 366 username, 367 hash, 368 algorithm); 369 } 370 } else { 371 throw new ConfigurationException( 372 String.format( 373 "Unsupported /httpServer/authentication/basic/algorithm [%s]", 374 algorithm)); 375 } 376 377 httpServerBuilder.withAuthenticator(authenticator); 378 } 379 } 380 381 /** 382 * Method to create a MessageDigestAuthenticator 383 * 384 * @param httpServerAuthenticationBasicYamlMapAccessor httpServerAuthenticationBasicMapAccessor 385 * @param realm realm 386 * @param username username 387 * @param password password 388 * @param algorithm algorithm 389 * @return a MessageDigestAuthenticator 390 */ 391 private Authenticator createMessageDigestAuthenticator( 392 YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor, 393 String realm, 394 String username, 395 String password, 396 String algorithm) { 397 String salt = 398 httpServerAuthenticationBasicYamlMapAccessor 399 .get("/salt") 400 .map( 401 new ConvertToString( 402 ConfigurationException.supplier( 403 "Invalid configuration for" 404 + " /httpServer/authentication/basic/salt must" 405 + " be a string"))) 406 .map( 407 new ValidateStringIsNotBlank( 408 ConfigurationException.supplier( 409 "Invalid configuration for" 410 + " /httpServer/authentication/basic/salt must" 411 + " not be blank"))) 412 .orElseThrow( 413 ConfigurationException.supplier( 414 "/httpServer/authentication/basic/salt is a required" 415 + " string")); 416 417 try { 418 return new MessageDigestAuthenticator(realm, username, password, algorithm, salt); 419 } catch (GeneralSecurityException e) { 420 throw new ConfigurationException( 421 String.format( 422 "Invalid /httpServer/authentication/basic/algorithm, unsupported" 423 + " algorithm [%s]", 424 algorithm)); 425 } 426 } 427 428 /** 429 * Method to create a PBKDF2Authenticator 430 * 431 * @param httpServerAuthenticationBasicYamlMapAccessor httpServerAuthenticationBasicMapAccessor 432 * @param realm realm 433 * @param username username 434 * @param password password 435 * @param algorithm algorithm 436 * @return a PBKDF2Authenticator 437 */ 438 private Authenticator createPBKDF2Authenticator( 439 YamlMapAccessor httpServerAuthenticationBasicYamlMapAccessor, 440 String realm, 441 String username, 442 String password, 443 String algorithm) { 444 String salt = 445 httpServerAuthenticationBasicYamlMapAccessor 446 .get("/salt") 447 .map( 448 new ConvertToString( 449 ConfigurationException.supplier( 450 "Invalid configuration for" 451 + " /httpServer/authentication/basic/salt must" 452 + " be a string"))) 453 .map( 454 new ValidateStringIsNotBlank( 455 ConfigurationException.supplier( 456 "Invalid configuration for" 457 + " /httpServer/authentication/basic/salt must" 458 + " be not blank"))) 459 .orElseThrow( 460 ConfigurationException.supplier( 461 "/httpServer/authentication/basic/salt is a required" 462 + " string")); 463 464 int iterations = 465 httpServerAuthenticationBasicYamlMapAccessor 466 .get("/iterations") 467 .map( 468 new ConvertToInteger( 469 ConfigurationException.supplier( 470 "Invalid configuration for" 471 + " /httpServer/authentication/basic/iterations" 472 + " must be an integer"))) 473 .map( 474 new ValidateIntegerInRange( 475 1, 476 Integer.MAX_VALUE, 477 ConfigurationException.supplier( 478 "Invalid configuration for" 479 + " /httpServer/authentication/basic/iterations" 480 + " must be between greater than 0"))) 481 .orElse(PBKDF2_ALGORITHM_ITERATIONS.get(algorithm)); 482 483 int keyLength = 484 httpServerAuthenticationBasicYamlMapAccessor 485 .get("/keyLength") 486 .map( 487 new ConvertToInteger( 488 ConfigurationException.supplier( 489 "Invalid configuration for" 490 + " /httpServer/authentication/basic/keyLength" 491 + " must be an integer"))) 492 .map( 493 new ValidateIntegerInRange( 494 1, 495 Integer.MAX_VALUE, 496 ConfigurationException.supplier( 497 "Invalid configuration for" 498 + " /httpServer/authentication/basic/keyLength" 499 + " must be greater than 0"))) 500 .orElse(PBKDF2_KEY_LENGTH_BITS); 501 502 try { 503 return new PBKDF2Authenticator( 504 realm, username, password, algorithm, salt, iterations, keyLength); 505 } catch (GeneralSecurityException e) { 506 throw new ConfigurationException( 507 String.format( 508 "Invalid /httpServer/authentication/basic/algorithm, unsupported" 509 + " algorithm [%s]", 510 algorithm)); 511 } 512 } 513 514 /** 515 * Method to configure SSL 516 * 517 * @param httpServerBuilder httpServerBuilder 518 */ 519 public void configureSSL(HTTPServer.Builder httpServerBuilder) { 520 if (rootYamlMapAccessor.containsPath("/httpServer/ssl")) { 521 try { 522 String keyStoreFilename = 523 rootYamlMapAccessor 524 .get("/httpServer/ssl/keyStore/filename") 525 .map( 526 new ConvertToString( 527 ConfigurationException.supplier( 528 "Invalid configuration for" 529 + " /httpServer/ssl/keyStore/filename" 530 + " must be a string"))) 531 .map( 532 new ValidateStringIsNotBlank( 533 ConfigurationException.supplier( 534 "Invalid configuration for" 535 + " /httpServer/ssl/keyStore/filename" 536 + " must not be blank"))) 537 .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE)); 538 539 String keyStorePassword = 540 rootYamlMapAccessor 541 .get("/httpServer/ssl/keyStore/password") 542 .map( 543 new ConvertToString( 544 ConfigurationException.supplier( 545 "Invalid configuration for" 546 + " /httpServer/ssl/keyStore/password" 547 + " must be a string"))) 548 .map( 549 new ValidateStringIsNotBlank( 550 ConfigurationException.supplier( 551 "Invalid configuration for" 552 + " /httpServer/ssl/keyStore/password" 553 + " must not be blank"))) 554 .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD)); 555 556 String certificateAlias = 557 rootYamlMapAccessor 558 .get("/httpServer/ssl/certificate/alias") 559 .map( 560 new ConvertToString( 561 ConfigurationException.supplier( 562 "Invalid configuration for" 563 + " /httpServer/ssl/certificate/alias" 564 + " must be a string"))) 565 .map( 566 new ValidateStringIsNotBlank( 567 ConfigurationException.supplier( 568 "Invalid configuration for" 569 + " /httpServer/ssl/certificate/alias" 570 + " must not be blank"))) 571 .orElseThrow( 572 ConfigurationException.supplier( 573 "/httpServer/ssl/certificate/alias is a required" 574 + " string")); 575 576 httpServerBuilder.withHttpsConfigurator( 577 new HttpsConfigurator( 578 SSLContextFactory.createSSLContext( 579 keyStoreFilename, keyStorePassword, certificateAlias))); 580 } catch (GeneralSecurityException | IOException e) { 581 String message = e.getMessage(); 582 if (message != null && !message.trim().isEmpty()) { 583 message = ", " + message.trim(); 584 } else { 585 message = ""; 586 } 587 588 throw new ConfigurationException( 589 String.format("Exception loading SSL configuration%s", message), e); 590 } 591 } 592 } 593 594 /** 595 * Class to implement a named thread factory 596 * 597 * <p>Copied from the `prometheus/client_java` `HTTPServer` due to scoping issues / dependencies 598 */ 599 private static class NamedDaemonThreadFactory implements ThreadFactory { 600 601 private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); 602 603 private final int poolNumber = POOL_NUMBER.getAndIncrement(); 604 private final AtomicInteger threadNumber = new AtomicInteger(1); 605 private final ThreadFactory delegate; 606 private final boolean daemon; 607 608 NamedDaemonThreadFactory(ThreadFactory delegate, boolean daemon) { 609 this.delegate = delegate; 610 this.daemon = daemon; 611 } 612 613 @Override 614 public Thread newThread(Runnable r) { 615 Thread t = delegate.newThread(r); 616 t.setName( 617 String.format( 618 "prometheus-http-%d-%d", poolNumber, threadNumber.getAndIncrement())); 619 t.setDaemon(daemon); 620 return t; 621 } 622 623 static ThreadFactory defaultThreadFactory(boolean daemon) { 624 return new NamedDaemonThreadFactory(Executors.defaultThreadFactory(), daemon); 625 } 626 } 627 628 /** Class to implement a blocking RejectedExecutionHandler */ 629 private static class BlockingRejectedExecutionHandler implements RejectedExecutionHandler { 630 631 @Override 632 public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) { 633 if (!threadPoolExecutor.isShutdown()) { 634 try { 635 threadPoolExecutor.getQueue().put(runnable); 636 } catch (InterruptedException e) { 637 // DO NOTHING 638 } 639 } 640 } 641 } 642}