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