001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.remotecontrol; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005 006import java.io.IOException; 007import java.io.InputStream; 008import java.io.OutputStream; 009import java.math.BigInteger; 010import java.net.ServerSocket; 011import java.net.Socket; 012import java.net.SocketException; 013import java.nio.file.Files; 014import java.nio.file.Path; 015import java.nio.file.Paths; 016import java.nio.file.StandardOpenOption; 017import java.security.GeneralSecurityException; 018import java.security.KeyPair; 019import java.security.KeyPairGenerator; 020import java.security.KeyStore; 021import java.security.KeyStoreException; 022import java.security.NoSuchAlgorithmException; 023import java.security.PrivateKey; 024import java.security.SecureRandom; 025import java.security.cert.Certificate; 026import java.security.cert.CertificateException; 027import java.security.cert.X509Certificate; 028import java.util.Arrays; 029import java.util.Date; 030import java.util.Enumeration; 031import java.util.Locale; 032import java.util.Vector; 033 034import javax.net.ssl.KeyManagerFactory; 035import javax.net.ssl.SSLContext; 036import javax.net.ssl.SSLServerSocket; 037import javax.net.ssl.SSLServerSocketFactory; 038import javax.net.ssl.SSLSocket; 039import javax.net.ssl.TrustManagerFactory; 040 041import org.openstreetmap.josm.data.preferences.StringProperty; 042import org.openstreetmap.josm.spi.preferences.Config; 043import org.openstreetmap.josm.tools.Logging; 044import org.openstreetmap.josm.tools.PlatformManager; 045 046import sun.security.util.ObjectIdentifier; 047import sun.security.x509.AlgorithmId; 048import sun.security.x509.BasicConstraintsExtension; 049import sun.security.x509.CertificateAlgorithmId; 050import sun.security.x509.CertificateExtensions; 051import sun.security.x509.CertificateSerialNumber; 052import sun.security.x509.CertificateValidity; 053import sun.security.x509.CertificateVersion; 054import sun.security.x509.CertificateX509Key; 055import sun.security.x509.DNSName; 056import sun.security.x509.ExtendedKeyUsageExtension; 057import sun.security.x509.GeneralName; 058import sun.security.x509.GeneralNameInterface; 059import sun.security.x509.GeneralNames; 060import sun.security.x509.IPAddressName; 061import sun.security.x509.OIDName; 062import sun.security.x509.SubjectAlternativeNameExtension; 063import sun.security.x509.URIName; 064import sun.security.x509.X500Name; 065import sun.security.x509.X509CertImpl; 066import sun.security.x509.X509CertInfo; 067 068/** 069 * Simple HTTPS server that spawns a {@link RequestProcessor} for every secure connection. 070 * 071 * @since 6941 072 */ 073public class RemoteControlHttpsServer extends Thread { 074 075 /** The server socket */ 076 private final ServerSocket server; 077 078 /** The server instance for IPv4 */ 079 private static volatile RemoteControlHttpsServer instance4; 080 /** The server instance for IPv6 */ 081 private static volatile RemoteControlHttpsServer instance6; 082 083 /** SSL context information for connections */ 084 private SSLContext sslContext; 085 086 /* the default port for HTTPS remote control */ 087 private static final int HTTPS_PORT = 8112; 088 089 /** 090 * JOSM keystore file name. 091 * @since 7337 092 */ 093 public static final String KEYSTORE_FILENAME = "josm.keystore"; 094 095 /** 096 * Preference for keystore password (automatically generated by JOSM). 097 * @since 7335 098 */ 099 public static final StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", ""); 100 101 /** 102 * Preference for certificate password (automatically generated by JOSM). 103 * @since 7335 104 */ 105 public static final StringProperty KEYENTRY_PASSWORD = new StringProperty("remotecontrol.https.keyentry.password", ""); 106 107 /** 108 * Unique alias used to store JOSM localhost entry, both in JOSM keystore and system/browser keystores. 109 * @since 7343 110 */ 111 public static final String ENTRY_ALIAS = "josm_localhost"; 112 113 /** 114 * Creates a GeneralNameInterface object from known types. 115 * @param t one of 4 known types 116 * @param v value 117 * @return which one 118 * @throws IOException if any I/O error occurs 119 */ 120 private static GeneralNameInterface createGeneralNameInterface(String t, String v) throws IOException { 121 switch (t.toLowerCase(Locale.ENGLISH)) { 122 case "uri": return new URIName(v); 123 case "dns": return new DNSName(v); 124 case "ip": return new IPAddressName(v); 125 default: return new OIDName(v); 126 } 127 } 128 129 /** 130 * Create a self-signed X.509 Certificate. 131 * @param dn the X.509 Distinguished Name, eg "CN=localhost, OU=JOSM, O=OpenStreetMap" 132 * @param pair the KeyPair 133 * @param days how many days from now the Certificate is valid for 134 * @param algorithm the signing algorithm, eg "SHA256withRSA" 135 * @param san SubjectAlternativeName extension (optional) 136 * @return the self-signed X.509 Certificate 137 * @throws GeneralSecurityException if any security error occurs 138 * @throws IOException if any I/O error occurs 139 */ 140 private static X509Certificate generateCertificate(String dn, KeyPair pair, int days, String algorithm, String san) 141 throws GeneralSecurityException, IOException { 142 X509CertInfo info = new X509CertInfo(); 143 Date from = new Date(); 144 Date to = new Date(from.getTime() + days * 86_400_000L); 145 CertificateValidity interval = new CertificateValidity(from, to); 146 BigInteger sn = new BigInteger(64, new SecureRandom()); 147 X500Name owner = new X500Name(dn); 148 149 info.set(X509CertInfo.VALIDITY, interval); 150 info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn)); 151 info.set(X509CertInfo.SUBJECT, owner); 152 info.set(X509CertInfo.ISSUER, owner); 153 154 info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic())); 155 info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); 156 AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid); 157 info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo)); 158 159 CertificateExtensions ext = new CertificateExtensions(); 160 // Critical: Not CA, max path len 0 161 ext.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(Boolean.TRUE, false, 0)); 162 // Critical: only allow TLS ("serverAuth" = 1.3.6.1.5.5.7.3.1) 163 ext.set(ExtendedKeyUsageExtension.NAME, new ExtendedKeyUsageExtension(Boolean.TRUE, 164 new Vector<>(Arrays.asList(new ObjectIdentifier("1.3.6.1.5.5.7.3.1"))))); 165 166 if (san != null) { 167 int colonpos; 168 String[] ps = san.split(","); 169 GeneralNames gnames = new GeneralNames(); 170 for (String item: ps) { 171 colonpos = item.indexOf(':'); 172 if (colonpos < 0) { 173 throw new IllegalArgumentException("Illegal item " + item + " in " + san); 174 } 175 String t = item.substring(0, colonpos); 176 String v = item.substring(colonpos+1); 177 gnames.add(new GeneralName(createGeneralNameInterface(t, v))); 178 } 179 // Non critical 180 ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(Boolean.FALSE, gnames)); 181 } 182 183 info.set(X509CertInfo.EXTENSIONS, ext); 184 185 // Sign the cert to identify the algorithm that's used. 186 PrivateKey privkey = pair.getPrivate(); 187 X509CertImpl cert = new X509CertImpl(info); 188 cert.sign(privkey, algorithm); 189 190 // Update the algorithm, and resign. 191 algo = (AlgorithmId) cert.get(X509CertImpl.SIG_ALG); 192 info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo); 193 cert = new X509CertImpl(info); 194 cert.sign(privkey, algorithm); 195 return cert; 196 } 197 198 /** 199 * Setup the JOSM internal keystore, used to store HTTPS certificate and private key. 200 * @return Path to the (initialized) JOSM keystore 201 * @throws IOException if an I/O error occurs 202 * @throws GeneralSecurityException if a security error occurs 203 * @since 7343 204 */ 205 public static Path setupJosmKeystore() throws IOException, GeneralSecurityException { 206 207 Path dir = Paths.get(RemoteControl.getRemoteControlDir()); 208 Path path = dir.resolve(KEYSTORE_FILENAME); 209 Files.createDirectories(dir); 210 211 if (!path.toFile().exists()) { 212 Logging.debug("No keystore found, creating a new one"); 213 214 // Create new keystore like previous one generated with JDK keytool as follows: 215 // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap" 216 // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825 217 218 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); 219 generator.initialize(2048); 220 KeyPair pair = generator.generateKeyPair(); 221 222 X509Certificate cert = generateCertificate("CN=localhost, OU=JOSM, O=OpenStreetMap", pair, 1825, "SHA256withRSA", 223 "dns:localhost,ip:127.0.0.1,ip:::1,uri:https://127.0.0.1:"+HTTPS_PORT+",uri:https://::1:"+HTTPS_PORT); 224 225 KeyStore ks = KeyStore.getInstance("JKS"); 226 ks.load(null, null); 227 228 // Generate new passwords. See https://stackoverflow.com/a/41156/2257172 229 SecureRandom random = new SecureRandom(); 230 KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32)); 231 KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32)); 232 233 char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray(); 234 char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray(); 235 236 ks.setKeyEntry(ENTRY_ALIAS, pair.getPrivate(), entryPassword, new Certificate[]{cert}); 237 try (OutputStream out = Files.newOutputStream(path, StandardOpenOption.CREATE)) { 238 ks.store(out, storePassword); 239 } 240 } 241 return path; 242 } 243 244 /** 245 * Loads the JOSM keystore. 246 * @return the (initialized) JOSM keystore 247 * @throws IOException if an I/O error occurs 248 * @throws GeneralSecurityException if a security error occurs 249 * @since 7343 250 */ 251 public static KeyStore loadJosmKeystore() throws IOException, GeneralSecurityException { 252 try (InputStream in = Files.newInputStream(setupJosmKeystore())) { 253 KeyStore ks = KeyStore.getInstance("JKS"); 254 ks.load(in, KEYSTORE_PASSWORD.get().toCharArray()); 255 256 if (Logging.isDebugEnabled()) { 257 for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) { 258 Logging.debug("Alias in JOSM keystore: {0}", aliases.nextElement()); 259 } 260 } 261 return ks; 262 } 263 } 264 265 /** 266 * Initializes the TLS basics. 267 * @throws IOException if an I/O error occurs 268 * @throws GeneralSecurityException if a security error occurs 269 */ 270 private void initialize() throws IOException, GeneralSecurityException { 271 KeyStore ks = loadJosmKeystore(); 272 273 KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); 274 kmf.init(ks, KEYENTRY_PASSWORD.get().toCharArray()); 275 276 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); 277 tmf.init(ks); 278 279 sslContext = SSLContext.getInstance("TLSv1.2"); 280 sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); 281 282 if (Logging.isTraceEnabled()) { 283 Logging.trace("SSL Context protocol: {0}", sslContext.getProtocol()); 284 Logging.trace("SSL Context provider: {0}", sslContext.getProvider()); 285 } 286 287 setupPlatform(ks); 288 } 289 290 /** 291 * Setup the platform-dependant certificate stuff. 292 * @param josmKs The JOSM keystore, containing localhost certificate and private key. 293 * @return {@code true} if something has changed as a result of the call (certificate installation, etc.) 294 * @throws KeyStoreException if the keystore has not been initialized (loaded) 295 * @throws NoSuchAlgorithmException in case of error 296 * @throws CertificateException in case of error 297 * @throws IOException in case of error 298 * @since 7343 299 */ 300 public static boolean setupPlatform(KeyStore josmKs) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { 301 Enumeration<String> aliases = josmKs.aliases(); 302 if (aliases.hasMoreElements()) { 303 return PlatformManager.getPlatform().setupHttpsCertificate(ENTRY_ALIAS, 304 new KeyStore.TrustedCertificateEntry(josmKs.getCertificate(aliases.nextElement()))); 305 } 306 return false; 307 } 308 309 /** 310 * Starts or restarts the HTTPS server 311 */ 312 public static void restartRemoteControlHttpsServer() { 313 stopRemoteControlHttpsServer(); 314 if (RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get()) { 315 int port = Config.getPref().getInt("remote.control.https.port", HTTPS_PORT); 316 try { 317 instance4 = new RemoteControlHttpsServer(port, false); 318 instance4.start(); 319 } catch (IOException | GeneralSecurityException ex) { 320 Logging.debug(ex); 321 Logging.warn(marktr("Cannot start IPv4 remotecontrol https server on port {0}: {1}"), 322 Integer.toString(port), ex.getLocalizedMessage()); 323 } 324 try { 325 instance6 = new RemoteControlHttpsServer(port, true); 326 instance6.start(); 327 } catch (IOException | GeneralSecurityException ex) { 328 /* only show error when we also have no IPv4 */ 329 if (instance4 == null) { 330 Logging.debug(ex); 331 Logging.warn(marktr("Cannot start IPv6 remotecontrol https server on port {0}: {1}"), 332 Integer.toString(port), ex.getLocalizedMessage()); 333 } 334 } 335 } 336 } 337 338 /** 339 * Stops the HTTPS server 340 */ 341 public static void stopRemoteControlHttpsServer() { 342 if (instance4 != null) { 343 try { 344 instance4.stopServer(); 345 } catch (IOException ioe) { 346 Logging.error(ioe); 347 } 348 instance4 = null; 349 } 350 if (instance6 != null) { 351 try { 352 instance6.stopServer(); 353 } catch (IOException ioe) { 354 Logging.error(ioe); 355 } 356 instance6 = null; 357 } 358 } 359 360 /** 361 * Constructs a new {@code RemoteControlHttpsServer}. 362 * @param port The port this server will listen on 363 * @param ipv6 Whether IPv6 or IPv4 server should be started 364 * @throws IOException when connection errors 365 * @throws GeneralSecurityException in case of SSL setup errors 366 * @since 8339 367 */ 368 public RemoteControlHttpsServer(int port, boolean ipv6) throws IOException, GeneralSecurityException { 369 super("RemoteControl HTTPS Server"); 370 this.setDaemon(true); 371 372 initialize(); 373 374 // Create SSL Server factory 375 SSLServerSocketFactory factory = sslContext.getServerSocketFactory(); 376 if (Logging.isTraceEnabled()) { 377 Logging.trace("SSL factory - Supported Cipher suites: {0}", Arrays.toString(factory.getSupportedCipherSuites())); 378 } 379 380 this.server = factory.createServerSocket(port, 1, ipv6 ? 381 RemoteControl.getInet6Address() : RemoteControl.getInet4Address()); 382 383 if (Logging.isTraceEnabled() && server instanceof SSLServerSocket) { 384 SSLServerSocket sslServer = (SSLServerSocket) server; 385 Logging.trace("SSL server - Enabled Cipher suites: {0}", Arrays.toString(sslServer.getEnabledCipherSuites())); 386 Logging.trace("SSL server - Enabled Protocols: {0}", Arrays.toString(sslServer.getEnabledProtocols())); 387 Logging.trace("SSL server - Enable Session Creation: {0}", sslServer.getEnableSessionCreation()); 388 Logging.trace("SSL server - Need Client Auth: {0}", sslServer.getNeedClientAuth()); 389 Logging.trace("SSL server - Want Client Auth: {0}", sslServer.getWantClientAuth()); 390 Logging.trace("SSL server - Use Client Mode: {0}", sslServer.getUseClientMode()); 391 } 392 } 393 394 /** 395 * The main loop, spawns a {@link RequestProcessor} for each connection. 396 */ 397 @Override 398 public void run() { 399 Logging.info(marktr("RemoteControl::Accepting secure remote connections on {0}:{1}"), 400 server.getInetAddress(), Integer.toString(server.getLocalPort())); 401 while (true) { 402 try { 403 @SuppressWarnings("resource") 404 Socket request = server.accept(); 405 if (Logging.isTraceEnabled() && request instanceof SSLSocket) { 406 SSLSocket sslSocket = (SSLSocket) request; 407 Logging.trace("SSL socket - Enabled Cipher suites: {0}", Arrays.toString(sslSocket.getEnabledCipherSuites())); 408 Logging.trace("SSL socket - Enabled Protocols: {0}", Arrays.toString(sslSocket.getEnabledProtocols())); 409 Logging.trace("SSL socket - Enable Session Creation: {0}", sslSocket.getEnableSessionCreation()); 410 Logging.trace("SSL socket - Need Client Auth: {0}", sslSocket.getNeedClientAuth()); 411 Logging.trace("SSL socket - Want Client Auth: {0}", sslSocket.getWantClientAuth()); 412 Logging.trace("SSL socket - Use Client Mode: {0}", sslSocket.getUseClientMode()); 413 Logging.trace("SSL socket - Session: {0}", sslSocket.getSession()); 414 } 415 RequestProcessor.processRequest(request); 416 } catch (SocketException e) { 417 if (!server.isClosed()) { 418 Logging.error(e); 419 } else { 420 // stop the thread automatically if server is stopped 421 return; 422 } 423 } catch (IOException ioe) { 424 Logging.error(ioe); 425 } 426 } 427 } 428 429 /** 430 * Stops the HTTPS server. 431 * 432 * @throws IOException if any I/O error occurs 433 */ 434 public void stopServer() throws IOException { 435 Logging.info(marktr("RemoteControl::Server {0}:{1} stopped."), 436 server.getInetAddress(), Integer.toString(server.getLocalPort())); 437 server.close(); 438 } 439}