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}