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