source: josm/trunk/src/org/openstreetmap/josm/io/remotecontrol/RemoteControlHttpsServer.java@ 8337

Last change on this file since 8337 was 8337, checked in by stoecker, 9 years ago

remotecontrol listens on IPv4 and IPv6 separately, fix #11409

  • Property svn:eol-style set to native
File size: 20.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.remotecontrol;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.IOException;
8import java.io.InputStream;
9import java.math.BigInteger;
10import java.net.BindException;
11import java.net.ServerSocket;
12import java.net.Socket;
13import java.net.SocketException;
14import java.nio.file.Files;
15import java.nio.file.Path;
16import java.nio.file.Paths;
17import java.nio.file.StandardOpenOption;
18import java.security.GeneralSecurityException;
19import java.security.KeyPair;
20import java.security.KeyPairGenerator;
21import java.security.KeyStore;
22import java.security.KeyStoreException;
23import java.security.NoSuchAlgorithmException;
24import java.security.PrivateKey;
25import java.security.SecureRandom;
26import java.security.cert.Certificate;
27import java.security.cert.CertificateException;
28import java.security.cert.X509Certificate;
29import java.util.Arrays;
30import java.util.Date;
31import java.util.Enumeration;
32import java.util.Vector;
33
34import javax.net.ssl.KeyManagerFactory;
35import javax.net.ssl.SSLContext;
36import javax.net.ssl.SSLServerSocket;
37import javax.net.ssl.SSLServerSocketFactory;
38import javax.net.ssl.SSLSocket;
39import javax.net.ssl.TrustManagerFactory;
40
41import org.openstreetmap.josm.Main;
42import org.openstreetmap.josm.data.preferences.StringProperty;
43
44import sun.security.util.ObjectIdentifier;
45import sun.security.x509.AlgorithmId;
46import sun.security.x509.BasicConstraintsExtension;
47import sun.security.x509.CertificateAlgorithmId;
48import sun.security.x509.CertificateExtensions;
49import sun.security.x509.CertificateIssuerName;
50import sun.security.x509.CertificateSerialNumber;
51import sun.security.x509.CertificateSubjectName;
52import sun.security.x509.CertificateValidity;
53import sun.security.x509.CertificateVersion;
54import sun.security.x509.CertificateX509Key;
55import sun.security.x509.ExtendedKeyUsageExtension;
56import sun.security.x509.GeneralName;
57import sun.security.x509.GeneralNameInterface;
58import sun.security.x509.GeneralNames;
59import sun.security.x509.IPAddressName;
60import sun.security.x509.OIDName;
61import sun.security.x509.SubjectAlternativeNameExtension;
62import sun.security.x509.URIName;
63import sun.security.x509.X500Name;
64import sun.security.x509.X509CertImpl;
65import sun.security.x509.X509CertInfo;
66
67/**
68 * Simple HTTPS server that spawns a {@link RequestProcessor} for every secure connection.
69 *
70 * @since 6941
71 */
72public class RemoteControlHttpsServer extends Thread {
73
74 /** The server socket for IPv4 */
75 private ServerSocket server4 = null;
76 /** The server socket for IPv6 */
77 private ServerSocket server6 = null;
78
79 private static volatile RemoteControlHttpsServer instance;
80 private boolean initOK = false;
81 private SSLContext sslContext;
82
83 private static final int HTTPS_PORT = 8112;
84
85 /**
86 * JOSM keystore file name.
87 * @since 7337
88 */
89 public static final String KEYSTORE_FILENAME = "josm.keystore";
90
91 /**
92 * Preference for keystore password (automatically generated by JOSM).
93 * @since 7335
94 */
95 public static final StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
96
97 /**
98 * Preference for certificate password (automatically generated by JOSM).
99 * @since 7335
100 */
101 public static final StringProperty KEYENTRY_PASSWORD = new StringProperty("remotecontrol.https.keyentry.password", "");
102
103 /**
104 * Unique alias used to store JOSM localhost entry, both in JOSM keystore and system/browser keystores.
105 * @since 7343
106 */
107 public static final String ENTRY_ALIAS = "josm_localhost";
108
109 /**
110 * Creates a GeneralName object from known types.
111 * @param t one of 4 known types
112 * @param v value
113 * @return which one
114 * @throws IOException
115 */
116 private static GeneralName createGeneralName(String t, String v) throws IOException {
117 GeneralNameInterface gn;
118 switch (t.toLowerCase()) {
119 case "uri": gn = new URIName(v); break;
120 case "dns": gn = new DNSName(v); break;
121 case "ip": gn = new IPAddressName(v); break;
122 default: gn = new OIDName(v);
123 }
124 return new GeneralName(gn);
125 }
126
127 /**
128 * Create a self-signed X.509 Certificate.
129 * @param dn the X.509 Distinguished Name, eg "CN=localhost, OU=JOSM, O=OpenStreetMap"
130 * @param pair the KeyPair
131 * @param days how many days from now the Certificate is valid for
132 * @param algorithm the signing algorithm, eg "SHA256withRSA"
133 * @param san SubjectAlternativeName extension (optional)
134 */
135 private static X509Certificate generateCertificate(String dn, KeyPair pair, int days, String algorithm, String san) throws GeneralSecurityException, IOException {
136 PrivateKey privkey = pair.getPrivate();
137 X509CertInfo info = new X509CertInfo();
138 Date from = new Date();
139 Date to = new Date(from.getTime() + days * 86400000L);
140 CertificateValidity interval = new CertificateValidity(from, to);
141 BigInteger sn = new BigInteger(64, new SecureRandom());
142 X500Name owner = new X500Name(dn);
143
144 info.set(X509CertInfo.VALIDITY, interval);
145 info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn));
146
147 // Change of behaviour in JDK8:
148 // https://bugs.openjdk.java.net/browse/JDK-8040820
149 // https://bugs.openjdk.java.net/browse/JDK-7198416
150 if (!Main.isJava8orLater()) {
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 try {
382 this.server4 = factory.createServerSocket(port, 1, RemoteControl.getInet4Address());
383 } catch (IOException e) {
384 }
385 try {
386 this.server6 = factory.createServerSocket(port, 1, RemoteControl.getInet6Address());
387 } catch (IOException e) {
388 if(this.server4 == null) /* both failed */
389 throw e;
390 }
391
392 if (Main.isTraceEnabled()) {
393 if(server4 instanceof SSLServerSocket) {
394 SSLServerSocket sslServer = (SSLServerSocket) server4;
395 Main.trace("SSL server - Enabled Cipher suites: "+Arrays.toString(sslServer.getEnabledCipherSuites()));
396 Main.trace("SSL server - Enabled Protocols: "+Arrays.toString(sslServer.getEnabledProtocols()));
397 Main.trace("SSL server - Enable Session Creation: "+sslServer.getEnableSessionCreation());
398 Main.trace("SSL server - Need Client Auth: "+sslServer.getNeedClientAuth());
399 Main.trace("SSL server - Want Client Auth: "+sslServer.getWantClientAuth());
400 Main.trace("SSL server - Use Client Mode: "+sslServer.getUseClientMode());
401 }
402 if(server6 instanceof SSLServerSocket) {
403 SSLServerSocket sslServer = (SSLServerSocket) server6;
404 Main.trace("SSL server - Enabled Cipher suites: "+Arrays.toString(sslServer.getEnabledCipherSuites()));
405 Main.trace("SSL server - Enabled Protocols: "+Arrays.toString(sslServer.getEnabledProtocols()));
406 Main.trace("SSL server - Enable Session Creation: "+sslServer.getEnableSessionCreation());
407 Main.trace("SSL server - Need Client Auth: "+sslServer.getNeedClientAuth());
408 Main.trace("SSL server - Want Client Auth: "+sslServer.getWantClientAuth());
409 Main.trace("SSL server - Use Client Mode: "+sslServer.getUseClientMode());
410 }
411 }
412 }
413
414 /**
415 * The main loop, spawns a {@link RequestProcessor} for each connection.
416 */
417 @Override
418 public void run() {
419 if(server4 != null) {
420 Main.info(marktr("RemoteControl::Accepting secure IPv4 connections on {0}:{1}"),
421 server4.getInetAddress(), Integer.toString(server4.getLocalPort()));
422 }
423 if(server6 != null) {
424 Main.info(marktr("RemoteControl::Accepting secure IPv6 connections on {0}:{1}"),
425 server6.getInetAddress(), Integer.toString(server6.getLocalPort()));
426 }
427 while (true) {
428 if(server4 != null) {
429 try {
430 @SuppressWarnings("resource")
431 Socket request = server4.accept();
432 if (Main.isTraceEnabled() && request instanceof SSLSocket) {
433 SSLSocket sslSocket = (SSLSocket) request;
434 Main.trace("SSL socket - Enabled Cipher suites: "+Arrays.toString(sslSocket.getEnabledCipherSuites()));
435 Main.trace("SSL socket - Enabled Protocols: "+Arrays.toString(sslSocket.getEnabledProtocols()));
436 Main.trace("SSL socket - Enable Session Creation: "+sslSocket.getEnableSessionCreation());
437 Main.trace("SSL socket - Need Client Auth: "+sslSocket.getNeedClientAuth());
438 Main.trace("SSL socket - Want Client Auth: "+sslSocket.getWantClientAuth());
439 Main.trace("SSL socket - Use Client Mode: "+sslSocket.getUseClientMode());
440 Main.trace("SSL socket - Session: "+sslSocket.getSession());
441 }
442 RequestProcessor.processRequest(request);
443 } catch (SocketException se) {
444 if (!server4.isClosed()) {
445 Main.error(se);
446 }
447 } catch (IOException ioe) {
448 Main.error(ioe);
449 }
450 }
451 if(server6 != null) {
452 try {
453 @SuppressWarnings("resource")
454 Socket request = server6.accept();
455 if (Main.isTraceEnabled() && request instanceof SSLSocket) {
456 SSLSocket sslSocket = (SSLSocket) request;
457 Main.trace("SSL socket - Enabled Cipher suites: "+Arrays.toString(sslSocket.getEnabledCipherSuites()));
458 Main.trace("SSL socket - Enabled Protocols: "+Arrays.toString(sslSocket.getEnabledProtocols()));
459 Main.trace("SSL socket - Enable Session Creation: "+sslSocket.getEnableSessionCreation());
460 Main.trace("SSL socket - Need Client Auth: "+sslSocket.getNeedClientAuth());
461 Main.trace("SSL socket - Want Client Auth: "+sslSocket.getWantClientAuth());
462 Main.trace("SSL socket - Use Client Mode: "+sslSocket.getUseClientMode());
463 Main.trace("SSL socket - Session: "+sslSocket.getSession());
464 }
465 RequestProcessor.processRequest(request);
466 } catch (SocketException se) {
467 if (!server6.isClosed()) {
468 Main.error(se);
469 }
470 } catch (IOException ioe) {
471 Main.error(ioe);
472 }
473 }
474 }
475 }
476
477 /**
478 * Stops the HTTPS server.
479 *
480 * @throws IOException if any I/O error occurs
481 */
482 public void stopServer() throws IOException {
483 if(server4 != null)
484 server4.close();
485 if(server6 != null)
486 server6.close();
487 if(server6 != null || server4 != null)
488 Main.info(marktr("RemoteControl::Server (IPv6 https) stopped."));
489 }
490}
Note: See TracBrowser for help on using the repository browser.