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

Last change on this file since 10212 was 10212, checked in by Don-vip, 8 years ago

sonar - squid:S2221 - "Exception" should not be caught when not required by called methods

  • Property svn:eol-style set to native
File size: 18.7 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;
5
6import java.io.IOException;
7import java.io.InputStream;
8import java.math.BigInteger;
9import java.net.ServerSocket;
10import java.net.Socket;
11import java.net.SocketException;
12import java.nio.file.Files;
13import java.nio.file.Path;
14import java.nio.file.Paths;
15import java.nio.file.StandardOpenOption;
16import java.security.GeneralSecurityException;
17import java.security.KeyPair;
18import java.security.KeyPairGenerator;
19import java.security.KeyStore;
20import java.security.KeyStoreException;
21import java.security.NoSuchAlgorithmException;
22import java.security.PrivateKey;
23import java.security.SecureRandom;
24import java.security.cert.Certificate;
25import java.security.cert.CertificateException;
26import java.security.cert.X509Certificate;
27import java.util.Arrays;
28import java.util.Date;
29import java.util.Enumeration;
30import java.util.Locale;
31import java.util.Vector;
32
33import javax.net.ssl.KeyManagerFactory;
34import javax.net.ssl.SSLContext;
35import javax.net.ssl.SSLServerSocket;
36import javax.net.ssl.SSLServerSocketFactory;
37import javax.net.ssl.SSLSocket;
38import javax.net.ssl.TrustManagerFactory;
39
40import org.openstreetmap.josm.Main;
41import org.openstreetmap.josm.data.preferences.StringProperty;
42
43import sun.security.util.ObjectIdentifier;
44import sun.security.x509.AlgorithmId;
45import sun.security.x509.BasicConstraintsExtension;
46import sun.security.x509.CertificateAlgorithmId;
47import sun.security.x509.CertificateExtensions;
48import sun.security.x509.CertificateIssuerName;
49import sun.security.x509.CertificateSerialNumber;
50import sun.security.x509.CertificateSubjectName;
51import sun.security.x509.CertificateValidity;
52import sun.security.x509.CertificateVersion;
53import sun.security.x509.CertificateX509Key;
54import sun.security.x509.ExtendedKeyUsageExtension;
55import sun.security.x509.GeneralName;
56import sun.security.x509.GeneralNameInterface;
57import sun.security.x509.GeneralNames;
58import sun.security.x509.IPAddressName;
59import sun.security.x509.OIDName;
60import sun.security.x509.SubjectAlternativeNameExtension;
61import sun.security.x509.URIName;
62import sun.security.x509.X500Name;
63import sun.security.x509.X509CertImpl;
64import sun.security.x509.X509CertInfo;
65
66/**
67 * Simple HTTPS server that spawns a {@link RequestProcessor} for every secure connection.
68 *
69 * @since 6941
70 */
71public class RemoteControlHttpsServer extends Thread {
72
73 /** The server socket */
74 private final ServerSocket server;
75
76 /** The server instance for IPv4 */
77 private static volatile RemoteControlHttpsServer instance4;
78 /** The server instance for IPv6 */
79 private static volatile RemoteControlHttpsServer instance6;
80
81 /** SSL context information for connections */
82 private SSLContext sslContext;
83
84 /* the default port for HTTPS remote control */
85 private static final int HTTPS_PORT = 8112;
86
87 /**
88 * JOSM keystore file name.
89 * @since 7337
90 */
91 public static final String KEYSTORE_FILENAME = "josm.keystore";
92
93 /**
94 * Preference for keystore password (automatically generated by JOSM).
95 * @since 7335
96 */
97 public static final StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
98
99 /**
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<>(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 (IOException | GeneralSecurityException 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 (IOException | GeneralSecurityException 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}
Note: See TracBrowser for help on using the repository browser.