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

Last change on this file since 12841 was 12841, checked in by bastiK, 7 years ago

see #15229 - fix deprecations caused by [12840]

  • Property svn:eol-style set to native
File size: 17.8 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;
43import org.openstreetmap.josm.tools.Logging;
44
45import sun.security.util.ObjectIdentifier;
46import sun.security.x509.AlgorithmId;
47import sun.security.x509.BasicConstraintsExtension;
48import sun.security.x509.CertificateAlgorithmId;
49import sun.security.x509.CertificateExtensions;
50import sun.security.x509.CertificateSerialNumber;
51import sun.security.x509.CertificateValidity;
52import sun.security.x509.CertificateVersion;
53import sun.security.x509.CertificateX509Key;
54import sun.security.x509.DNSName;
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 */
75 private final ServerSocket server;
76
77 /** The server instance for IPv4 */
78 private static volatile RemoteControlHttpsServer instance4;
79 /** The server instance for IPv6 */
80 private static volatile RemoteControlHttpsServer instance6;
81
82 /** SSL context information for connections */
83 private SSLContext sslContext;
84
85 /* the default port for HTTPS remote control */
86 private static final int HTTPS_PORT = 8112;
87
88 /**
89 * JOSM keystore file name.
90 * @since 7337
91 */
92 public static final String KEYSTORE_FILENAME = "josm.keystore";
93
94 /**
95 * Preference for keystore password (automatically generated by JOSM).
96 * @since 7335
97 */
98 public static final StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
99
100 /**
101 * Preference for certificate password (automatically generated by JOSM).
102 * @since 7335
103 */
104 public static final StringProperty KEYENTRY_PASSWORD = new StringProperty("remotecontrol.https.keyentry.password", "");
105
106 /**
107 * Unique alias used to store JOSM localhost entry, both in JOSM keystore and system/browser keystores.
108 * @since 7343
109 */
110 public static final String ENTRY_ALIAS = "josm_localhost";
111
112 /**
113 * Creates a GeneralNameInterface object from known types.
114 * @param t one of 4 known types
115 * @param v value
116 * @return which one
117 * @throws IOException if any I/O error occurs
118 */
119 private static GeneralNameInterface createGeneralNameInterface(String t, String v) throws IOException {
120 switch (t.toLowerCase(Locale.ENGLISH)) {
121 case "uri": return new URIName(v);
122 case "dns": return new DNSName(v);
123 case "ip": return new IPAddressName(v);
124 default: return new OIDName(v);
125 }
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(new GeneralName(createGeneralNameInterface(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 Logging.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 "dns:localhost,ip:127.0.0.1,ip:::1,uri:https://127.0.0.1:"+HTTPS_PORT+",uri:https://::1:"+HTTPS_PORT);
223
224 KeyStore ks = KeyStore.getInstance("JKS");
225 ks.load(null, null);
226
227 // Generate new passwords. See https://stackoverflow.com/a/41156/2257172
228 SecureRandom random = new SecureRandom();
229 KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32));
230 KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32));
231
232 char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray();
233 char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
234
235 ks.setKeyEntry(ENTRY_ALIAS, pair.getPrivate(), entryPassword, new Certificate[]{cert});
236 try (OutputStream out = Files.newOutputStream(path, StandardOpenOption.CREATE)) {
237 ks.store(out, storePassword);
238 }
239 }
240 return path;
241 }
242
243 /**
244 * Loads the JOSM keystore.
245 * @return the (initialized) JOSM keystore
246 * @throws IOException if an I/O error occurs
247 * @throws GeneralSecurityException if a security error occurs
248 * @since 7343
249 */
250 public static KeyStore loadJosmKeystore() throws IOException, GeneralSecurityException {
251 try (InputStream in = Files.newInputStream(setupJosmKeystore())) {
252 KeyStore ks = KeyStore.getInstance("JKS");
253 ks.load(in, KEYSTORE_PASSWORD.get().toCharArray());
254
255 if (Logging.isDebugEnabled()) {
256 for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
257 Logging.debug("Alias in JOSM keystore: {0}", aliases.nextElement());
258 }
259 }
260 return ks;
261 }
262 }
263
264 /**
265 * Initializes the TLS basics.
266 * @throws IOException if an I/O error occurs
267 * @throws GeneralSecurityException if a security error occurs
268 */
269 private void initialize() throws IOException, GeneralSecurityException {
270 KeyStore ks = loadJosmKeystore();
271
272 KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
273 kmf.init(ks, KEYENTRY_PASSWORD.get().toCharArray());
274
275 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
276 tmf.init(ks);
277
278 sslContext = SSLContext.getInstance("TLS");
279 sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
280
281 if (Logging.isTraceEnabled()) {
282 Logging.trace("SSL Context protocol: {0}", sslContext.getProtocol());
283 Logging.trace("SSL Context provider: {0}", sslContext.getProvider());
284 }
285
286 setupPlatform(ks);
287 }
288
289 /**
290 * Setup the platform-dependant certificate stuff.
291 * @param josmKs The JOSM keystore, containing localhost certificate and private key.
292 * @return {@code true} if something has changed as a result of the call (certificate installation, etc.)
293 * @throws KeyStoreException if the keystore has not been initialized (loaded)
294 * @throws NoSuchAlgorithmException in case of error
295 * @throws CertificateException in case of error
296 * @throws IOException in case of error
297 * @since 7343
298 */
299 public static boolean setupPlatform(KeyStore josmKs) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
300 Enumeration<String> aliases = josmKs.aliases();
301 if (aliases.hasMoreElements()) {
302 return Main.platform.setupHttpsCertificate(ENTRY_ALIAS,
303 new KeyStore.TrustedCertificateEntry(josmKs.getCertificate(aliases.nextElement())));
304 }
305 return false;
306 }
307
308 /**
309 * Starts or restarts the HTTPS server
310 */
311 public static void restartRemoteControlHttpsServer() {
312 stopRemoteControlHttpsServer();
313 if (RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get()) {
314 int port = Main.pref.getInt("remote.control.https.port", HTTPS_PORT);
315 try {
316 instance4 = new RemoteControlHttpsServer(port, false);
317 instance4.start();
318 } catch (IOException | GeneralSecurityException ex) {
319 Logging.debug(ex);
320 Logging.warn(marktr("Cannot start IPv4 remotecontrol https server on port {0}: {1}"),
321 Integer.toString(port), ex.getLocalizedMessage());
322 }
323 try {
324 instance6 = new RemoteControlHttpsServer(port, true);
325 instance6.start();
326 } catch (IOException | GeneralSecurityException ex) {
327 /* only show error when we also have no IPv4 */
328 if (instance4 == null) {
329 Logging.debug(ex);
330 Logging.warn(marktr("Cannot start IPv6 remotecontrol https server on port {0}: {1}"),
331 Integer.toString(port), ex.getLocalizedMessage());
332 }
333 }
334 }
335 }
336
337 /**
338 * Stops the HTTPS server
339 */
340 public static void stopRemoteControlHttpsServer() {
341 if (instance4 != null) {
342 try {
343 instance4.stopServer();
344 } catch (IOException ioe) {
345 Logging.error(ioe);
346 }
347 instance4 = null;
348 }
349 if (instance6 != null) {
350 try {
351 instance6.stopServer();
352 } catch (IOException ioe) {
353 Logging.error(ioe);
354 }
355 instance6 = null;
356 }
357 }
358
359 /**
360 * Constructs a new {@code RemoteControlHttpsServer}.
361 * @param port The port this server will listen on
362 * @param ipv6 Whether IPv6 or IPv4 server should be started
363 * @throws IOException when connection errors
364 * @throws GeneralSecurityException in case of SSL setup errors
365 * @since 8339
366 */
367 public RemoteControlHttpsServer(int port, boolean ipv6) throws IOException, GeneralSecurityException {
368 super("RemoteControl HTTPS Server");
369 this.setDaemon(true);
370
371 initialize();
372
373 // Create SSL Server factory
374 SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
375 if (Logging.isTraceEnabled()) {
376 Logging.trace("SSL factory - Supported Cipher suites: {0}", Arrays.toString(factory.getSupportedCipherSuites()));
377 }
378
379 this.server = factory.createServerSocket(port, 1, ipv6 ?
380 RemoteControl.getInet6Address() : RemoteControl.getInet4Address());
381
382 if (Logging.isTraceEnabled() && server instanceof SSLServerSocket) {
383 SSLServerSocket sslServer = (SSLServerSocket) server;
384 Logging.trace("SSL server - Enabled Cipher suites: {0}", Arrays.toString(sslServer.getEnabledCipherSuites()));
385 Logging.trace("SSL server - Enabled Protocols: {0}", Arrays.toString(sslServer.getEnabledProtocols()));
386 Logging.trace("SSL server - Enable Session Creation: {0}", sslServer.getEnableSessionCreation());
387 Logging.trace("SSL server - Need Client Auth: {0}", sslServer.getNeedClientAuth());
388 Logging.trace("SSL server - Want Client Auth: {0}", sslServer.getWantClientAuth());
389 Logging.trace("SSL server - Use Client Mode: {0}", sslServer.getUseClientMode());
390 }
391 }
392
393 /**
394 * The main loop, spawns a {@link RequestProcessor} for each connection.
395 */
396 @Override
397 public void run() {
398 Logging.info(marktr("RemoteControl::Accepting secure remote connections on {0}:{1}"),
399 server.getInetAddress(), Integer.toString(server.getLocalPort()));
400 while (true) {
401 try {
402 @SuppressWarnings("resource")
403 Socket request = server.accept();
404 if (Logging.isTraceEnabled() && request instanceof SSLSocket) {
405 SSLSocket sslSocket = (SSLSocket) request;
406 Logging.trace("SSL socket - Enabled Cipher suites: {0}", Arrays.toString(sslSocket.getEnabledCipherSuites()));
407 Logging.trace("SSL socket - Enabled Protocols: {0}", Arrays.toString(sslSocket.getEnabledProtocols()));
408 Logging.trace("SSL socket - Enable Session Creation: {0}", sslSocket.getEnableSessionCreation());
409 Logging.trace("SSL socket - Need Client Auth: {0}", sslSocket.getNeedClientAuth());
410 Logging.trace("SSL socket - Want Client Auth: {0}", sslSocket.getWantClientAuth());
411 Logging.trace("SSL socket - Use Client Mode: {0}", sslSocket.getUseClientMode());
412 Logging.trace("SSL socket - Session: {0}", sslSocket.getSession());
413 }
414 RequestProcessor.processRequest(request);
415 } catch (SocketException e) {
416 if (!server.isClosed()) {
417 Logging.error(e);
418 }
419 } catch (IOException ioe) {
420 Logging.error(ioe);
421 }
422 }
423 }
424
425 /**
426 * Stops the HTTPS server.
427 *
428 * @throws IOException if any I/O error occurs
429 */
430 public void stopServer() throws IOException {
431 Logging.info(marktr("RemoteControl::Server {0}:{1} stopped."),
432 server.getInetAddress(), Integer.toString(server.getLocalPort()));
433 server.close();
434 }
435}
Note: See TracBrowser for help on using the repository browser.