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

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

findbugs - OS_OPEN_STREAM

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