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

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

fix remaining checkstyle issues

  • Property svn:eol-style set to native
File size: 18.5 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 ServerSocket server = null;
75
76 /** The server instance for IPv4 */
77 private static volatile RemoteControlHttpsServer instance4 = null;
78 /** The server instance for IPv6 */
79 private static volatile RemoteControlHttpsServer instance6 = null;
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 */
137 private static X509Certificate generateCertificate(String dn, KeyPair pair, int days, String algorithm, String san)
138 throws GeneralSecurityException, IOException {
139 PrivateKey privkey = pair.getPrivate();
140 X509CertInfo info = new X509CertInfo();
141 Date from = new Date();
142 Date to = new Date(from.getTime() + days * 86400000L);
143 CertificateValidity interval = new CertificateValidity(from, to);
144 BigInteger sn = new BigInteger(64, new SecureRandom());
145 X500Name owner = new X500Name(dn);
146
147 info.set(X509CertInfo.VALIDITY, interval);
148 info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn));
149
150 // Change of behaviour in JDK8:
151 // https://bugs.openjdk.java.net/browse/JDK-8040820
152 // https://bugs.openjdk.java.net/browse/JDK-7198416
153 if (!Main.isJava8orLater()) {
154 // Java 7 code. To remove with Java 8 migration
155 info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(owner));
156 info.set(X509CertInfo.ISSUER, new CertificateIssuerName(owner));
157 } else {
158 // Java 8 and later code
159 info.set(X509CertInfo.SUBJECT, owner);
160 info.set(X509CertInfo.ISSUER, owner);
161 }
162
163 info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic()));
164 info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
165 AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
166 info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo));
167
168 CertificateExtensions ext = new CertificateExtensions();
169 // Critical: Not CA, max path len 0
170 ext.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(Boolean.TRUE, false, 0));
171 // Critical: only allow TLS ("serverAuth" = 1.3.6.1.5.5.7.3.1)
172 ext.set(ExtendedKeyUsageExtension.NAME, new ExtendedKeyUsageExtension(Boolean.TRUE,
173 new Vector<ObjectIdentifier>(Arrays.asList(new ObjectIdentifier("1.3.6.1.5.5.7.3.1")))));
174
175 if (san != null) {
176 int colonpos;
177 String[] ps = san.split(",");
178 GeneralNames gnames = new GeneralNames();
179 for (String item: ps) {
180 colonpos = item.indexOf(':');
181 if (colonpos < 0) {
182 throw new IllegalArgumentException("Illegal item " + item + " in " + san);
183 }
184 String t = item.substring(0, colonpos);
185 String v = item.substring(colonpos+1);
186 gnames.add(createGeneralName(t, v));
187 }
188 // Non critical
189 ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(Boolean.FALSE, gnames));
190 }
191
192 info.set(X509CertInfo.EXTENSIONS, ext);
193
194 // Sign the cert to identify the algorithm that's used.
195 X509CertImpl cert = new X509CertImpl(info);
196 cert.sign(privkey, algorithm);
197
198 // Update the algorithm, and resign.
199 algo = (AlgorithmId) cert.get(X509CertImpl.SIG_ALG);
200 info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo);
201 cert = new X509CertImpl(info);
202 cert.sign(privkey, algorithm);
203 return cert;
204 }
205
206 /**
207 * Setup the JOSM internal keystore, used to store HTTPS certificate and private key.
208 * @return Path to the (initialized) JOSM keystore
209 * @throws IOException if an I/O error occurs
210 * @throws GeneralSecurityException if a security error occurs
211 * @since 7343
212 */
213 public static Path setupJosmKeystore() throws IOException, GeneralSecurityException {
214
215 char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray();
216 char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
217
218 Path dir = Paths.get(RemoteControl.getRemoteControlDir());
219 Path path = dir.resolve(KEYSTORE_FILENAME);
220 Files.createDirectories(dir);
221
222 if (!Files.exists(path)) {
223 Main.debug("No keystore found, creating a new one");
224
225 // Create new keystore like previous one generated with JDK keytool as follows:
226 // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap"
227 // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825
228
229 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
230 generator.initialize(2048);
231 KeyPair pair = generator.generateKeyPair();
232
233 X509Certificate cert = generateCertificate("CN=localhost, OU=JOSM, O=OpenStreetMap", pair, 1825, "SHA256withRSA",
234 // see #10033#comment:20: All browsers respect "ip" in SAN, except IE which only understands DNS entries:
235 // CHECKSTYLE.OFF: LineLength
236 // https://connect.microsoft.com/IE/feedback/details/814744/the-ie-doesnt-trust-a-san-certificate-when-connecting-to-ip-address
237 // CHECKSTYLE.ON: LineLength
238 "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);
239
240 KeyStore ks = KeyStore.getInstance("JKS");
241 ks.load(null, null);
242
243 // Generate new passwords. See https://stackoverflow.com/a/41156/2257172
244 SecureRandom random = new SecureRandom();
245 KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32));
246 KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32));
247
248 storePassword = KEYSTORE_PASSWORD.get().toCharArray();
249 entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
250
251 ks.setKeyEntry(ENTRY_ALIAS, pair.getPrivate(), entryPassword, new Certificate[]{cert});
252 ks.store(Files.newOutputStream(path, StandardOpenOption.CREATE), storePassword);
253 }
254 return path;
255 }
256
257 /**
258 * Loads the JOSM keystore.
259 * @return the (initialized) JOSM keystore
260 * @throws IOException if an I/O error occurs
261 * @throws GeneralSecurityException if a security error occurs
262 * @since 7343
263 */
264 public static KeyStore loadJosmKeystore() throws IOException, GeneralSecurityException {
265 try (InputStream in = Files.newInputStream(setupJosmKeystore())) {
266 KeyStore ks = KeyStore.getInstance("JKS");
267 ks.load(in, KEYSTORE_PASSWORD.get().toCharArray());
268
269 if (Main.isDebugEnabled()) {
270 for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
271 Main.debug("Alias in JOSM keystore: "+aliases.nextElement());
272 }
273 }
274 return ks;
275 }
276 }
277
278 /**
279 * Initializes the TLS basics.
280 * @throws IOException if an I/O error occurs
281 * @throws GeneralSecurityException if a security error occurs
282 */
283 private void initialize() throws IOException, GeneralSecurityException {
284 KeyStore ks = loadJosmKeystore();
285
286 KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
287 kmf.init(ks, KEYENTRY_PASSWORD.get().toCharArray());
288
289 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
290 tmf.init(ks);
291
292 sslContext = SSLContext.getInstance("TLS");
293 sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
294
295 if (Main.isTraceEnabled()) {
296 Main.trace("SSL Context protocol: " + sslContext.getProtocol());
297 Main.trace("SSL Context provider: " + sslContext.getProvider());
298 }
299
300 setupPlatform(ks);
301 }
302
303 /**
304 * Setup the platform-dependant certificate stuff.
305 * @param josmKs The JOSM keystore, containing localhost certificate and private key.
306 * @return {@code true} if something has changed as a result of the call (certificate installation, etc.)
307 * @throws KeyStoreException if the keystore has not been initialized (loaded)
308 * @throws NoSuchAlgorithmException in case of error
309 * @throws CertificateException in case of error
310 * @throws IOException in case of error
311 * @since 7343
312 */
313 public static boolean setupPlatform(KeyStore josmKs) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
314 Enumeration<String> aliases = josmKs.aliases();
315 if (aliases.hasMoreElements()) {
316 return Main.platform.setupHttpsCertificate(ENTRY_ALIAS,
317 new KeyStore.TrustedCertificateEntry(josmKs.getCertificate(aliases.nextElement())));
318 }
319 return false;
320 }
321
322 /**
323 * Starts or restarts the HTTPS server
324 */
325 public static void restartRemoteControlHttpsServer() {
326 stopRemoteControlHttpsServer();
327 if (RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get()) {
328 int port = Main.pref.getInteger("remote.control.https.port", HTTPS_PORT);
329 try {
330 instance4 = new RemoteControlHttpsServer(port, false);
331 instance4.start();
332 } catch (Exception ex) {
333 Main.warn(marktr("Cannot start IPv4 remotecontrol https server on port {0}: {1}"),
334 Integer.toString(port), ex.getLocalizedMessage());
335 }
336 try {
337 instance6 = new RemoteControlHttpsServer(port, true);
338 instance6.start();
339 } catch (Exception ex) {
340 /* only show error when we also have no IPv4 */
341 if (instance4 == null) {
342 Main.warn(marktr("Cannot start IPv6 remotecontrol https server on port {0}: {1}"),
343 Integer.toString(port), ex.getLocalizedMessage());
344 }
345 }
346 }
347 }
348
349 /**
350 * Stops the HTTPS server
351 */
352 public static void stopRemoteControlHttpsServer() {
353 if (instance4 != null) {
354 try {
355 instance4.stopServer();
356 } catch (IOException ioe) {
357 Main.error(ioe);
358 }
359 instance4 = null;
360 }
361 if (instance6 != null) {
362 try {
363 instance6.stopServer();
364 } catch (IOException ioe) {
365 Main.error(ioe);
366 }
367 instance6 = null;
368 }
369 }
370
371 /**
372 * Constructs a new {@code RemoteControlHttpsServer}.
373 * @param port The port this server will listen on
374 * @param ipv6 Whether IPv6 or IPv4 server should be started
375 * @throws IOException when connection errors
376 * @throws NoSuchAlgorithmException if the JVM does not support TLS (can not happen)
377 * @throws GeneralSecurityException in case of SSL setup errors
378 * @since 8339
379 */
380 public RemoteControlHttpsServer(int port, boolean ipv6) throws IOException, NoSuchAlgorithmException, GeneralSecurityException {
381 super("RemoteControl HTTPS Server");
382 this.setDaemon(true);
383
384 initialize();
385
386 // Create SSL Server factory
387 SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
388 if (Main.isTraceEnabled()) {
389 Main.trace("SSL factory - Supported Cipher suites: "+Arrays.toString(factory.getSupportedCipherSuites()));
390 }
391
392 this.server = factory.createServerSocket(port, 1, ipv6 ?
393 RemoteControl.getInet6Address() : RemoteControl.getInet4Address());
394
395 if (Main.isTraceEnabled()) {
396 if (server instanceof SSLServerSocket) {
397 SSLServerSocket sslServer = (SSLServerSocket) server;
398 Main.trace("SSL server - Enabled Cipher suites: "+Arrays.toString(sslServer.getEnabledCipherSuites()));
399 Main.trace("SSL server - Enabled Protocols: "+Arrays.toString(sslServer.getEnabledProtocols()));
400 Main.trace("SSL server - Enable Session Creation: "+sslServer.getEnableSessionCreation());
401 Main.trace("SSL server - Need Client Auth: "+sslServer.getNeedClientAuth());
402 Main.trace("SSL server - Want Client Auth: "+sslServer.getWantClientAuth());
403 Main.trace("SSL server - Use Client Mode: "+sslServer.getUseClientMode());
404 }
405 }
406 }
407
408 /**
409 * The main loop, spawns a {@link RequestProcessor} for each connection.
410 */
411 @Override
412 public void run() {
413 Main.info(marktr("RemoteControl::Accepting secure remote connections on {0}:{1}"),
414 server.getInetAddress(), Integer.toString(server.getLocalPort()));
415 while (true) {
416 try {
417 @SuppressWarnings("resource")
418 Socket request = server.accept();
419 if (Main.isTraceEnabled() && request instanceof SSLSocket) {
420 SSLSocket sslSocket = (SSLSocket) request;
421 Main.trace("SSL socket - Enabled Cipher suites: "+Arrays.toString(sslSocket.getEnabledCipherSuites()));
422 Main.trace("SSL socket - Enabled Protocols: "+Arrays.toString(sslSocket.getEnabledProtocols()));
423 Main.trace("SSL socket - Enable Session Creation: "+sslSocket.getEnableSessionCreation());
424 Main.trace("SSL socket - Need Client Auth: "+sslSocket.getNeedClientAuth());
425 Main.trace("SSL socket - Want Client Auth: "+sslSocket.getWantClientAuth());
426 Main.trace("SSL socket - Use Client Mode: "+sslSocket.getUseClientMode());
427 Main.trace("SSL socket - Session: "+sslSocket.getSession());
428 }
429 RequestProcessor.processRequest(request);
430 } catch (SocketException se) {
431 if (!server.isClosed()) {
432 Main.error(se);
433 }
434 } catch (IOException ioe) {
435 Main.error(ioe);
436 }
437 }
438 }
439
440 /**
441 * Stops the HTTPS server.
442 *
443 * @throws IOException if any I/O error occurs
444 */
445 public void stopServer() throws IOException {
446 Main.info(marktr("RemoteControl::Server {0}:{1} stopped."),
447 server.getInetAddress(), Integer.toString(server.getLocalPort()));
448 server.close();
449 }
450}
Note: See TracBrowser for help on using the repository browser.