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