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