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

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

see #15229 - use Config.getPref() wherever possible

  • Property svn:eol-style set to native
File size: 17.9 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.spi.preferences.Config;
44import org.openstreetmap.josm.tools.Logging;
45
46import sun.security.util.ObjectIdentifier;
47import sun.security.x509.AlgorithmId;
48import sun.security.x509.BasicConstraintsExtension;
49import sun.security.x509.CertificateAlgorithmId;
50import sun.security.x509.CertificateExtensions;
51import sun.security.x509.CertificateSerialNumber;
52import sun.security.x509.CertificateValidity;
53import sun.security.x509.CertificateVersion;
54import sun.security.x509.CertificateX509Key;
55import sun.security.x509.DNSName;
56import sun.security.x509.ExtendedKeyUsageExtension;
57import sun.security.x509.GeneralName;
58import sun.security.x509.GeneralNameInterface;
59import sun.security.x509.GeneralNames;
60import sun.security.x509.IPAddressName;
61import sun.security.x509.OIDName;
62import sun.security.x509.SubjectAlternativeNameExtension;
63import sun.security.x509.URIName;
64import sun.security.x509.X500Name;
65import sun.security.x509.X509CertImpl;
66import sun.security.x509.X509CertInfo;
67
68/**
69 * Simple HTTPS server that spawns a {@link RequestProcessor} for every secure connection.
70 *
71 * @since 6941
72 */
73public class RemoteControlHttpsServer extends Thread {
74
75 /** The server socket */
76 private final ServerSocket server;
77
78 /** The server instance for IPv4 */
79 private static volatile RemoteControlHttpsServer instance4;
80 /** The server instance for IPv6 */
81 private static volatile RemoteControlHttpsServer instance6;
82
83 /** SSL context information for connections */
84 private SSLContext sslContext;
85
86 /* the default port for HTTPS remote control */
87 private static final int HTTPS_PORT = 8112;
88
89 /**
90 * JOSM keystore file name.
91 * @since 7337
92 */
93 public static final String KEYSTORE_FILENAME = "josm.keystore";
94
95 /**
96 * Preference for keystore password (automatically generated by JOSM).
97 * @since 7335
98 */
99 public static final StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
100
101 /**
102 * Preference for certificate password (automatically generated by JOSM).
103 * @since 7335
104 */
105 public static final StringProperty KEYENTRY_PASSWORD = new StringProperty("remotecontrol.https.keyentry.password", "");
106
107 /**
108 * Unique alias used to store JOSM localhost entry, both in JOSM keystore and system/browser keystores.
109 * @since 7343
110 */
111 public static final String ENTRY_ALIAS = "josm_localhost";
112
113 /**
114 * Creates a GeneralNameInterface object from known types.
115 * @param t one of 4 known types
116 * @param v value
117 * @return which one
118 * @throws IOException if any I/O error occurs
119 */
120 private static GeneralNameInterface createGeneralNameInterface(String t, String v) throws IOException {
121 switch (t.toLowerCase(Locale.ENGLISH)) {
122 case "uri": return new URIName(v);
123 case "dns": return new DNSName(v);
124 case "ip": return new IPAddressName(v);
125 default: return new OIDName(v);
126 }
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 * 86_400_000L);
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 info.set(X509CertInfo.SUBJECT, owner);
152 info.set(X509CertInfo.ISSUER, owner);
153
154 info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic()));
155 info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
156 AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
157 info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo));
158
159 CertificateExtensions ext = new CertificateExtensions();
160 // Critical: Not CA, max path len 0
161 ext.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(Boolean.TRUE, false, 0));
162 // Critical: only allow TLS ("serverAuth" = 1.3.6.1.5.5.7.3.1)
163 ext.set(ExtendedKeyUsageExtension.NAME, new ExtendedKeyUsageExtension(Boolean.TRUE,
164 new Vector<>(Arrays.asList(new ObjectIdentifier("1.3.6.1.5.5.7.3.1")))));
165
166 if (san != null) {
167 int colonpos;
168 String[] ps = san.split(",");
169 GeneralNames gnames = new GeneralNames();
170 for (String item: ps) {
171 colonpos = item.indexOf(':');
172 if (colonpos < 0) {
173 throw new IllegalArgumentException("Illegal item " + item + " in " + san);
174 }
175 String t = item.substring(0, colonpos);
176 String v = item.substring(colonpos+1);
177 gnames.add(new GeneralName(createGeneralNameInterface(t, v)));
178 }
179 // Non critical
180 ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(Boolean.FALSE, gnames));
181 }
182
183 info.set(X509CertInfo.EXTENSIONS, ext);
184
185 // Sign the cert to identify the algorithm that's used.
186 PrivateKey privkey = pair.getPrivate();
187 X509CertImpl cert = new X509CertImpl(info);
188 cert.sign(privkey, algorithm);
189
190 // Update the algorithm, and resign.
191 algo = (AlgorithmId) cert.get(X509CertImpl.SIG_ALG);
192 info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo);
193 cert = new X509CertImpl(info);
194 cert.sign(privkey, algorithm);
195 return cert;
196 }
197
198 /**
199 * Setup the JOSM internal keystore, used to store HTTPS certificate and private key.
200 * @return Path to the (initialized) JOSM keystore
201 * @throws IOException if an I/O error occurs
202 * @throws GeneralSecurityException if a security error occurs
203 * @since 7343
204 */
205 public static Path setupJosmKeystore() throws IOException, GeneralSecurityException {
206
207 Path dir = Paths.get(RemoteControl.getRemoteControlDir());
208 Path path = dir.resolve(KEYSTORE_FILENAME);
209 Files.createDirectories(dir);
210
211 if (!path.toFile().exists()) {
212 Logging.debug("No keystore found, creating a new one");
213
214 // Create new keystore like previous one generated with JDK keytool as follows:
215 // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap"
216 // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825
217
218 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
219 generator.initialize(2048);
220 KeyPair pair = generator.generateKeyPair();
221
222 X509Certificate cert = generateCertificate("CN=localhost, OU=JOSM, O=OpenStreetMap", pair, 1825, "SHA256withRSA",
223 "dns:localhost,ip:127.0.0.1,ip:::1,uri:https://127.0.0.1:"+HTTPS_PORT+",uri:https://::1:"+HTTPS_PORT);
224
225 KeyStore ks = KeyStore.getInstance("JKS");
226 ks.load(null, null);
227
228 // Generate new passwords. See https://stackoverflow.com/a/41156/2257172
229 SecureRandom random = new SecureRandom();
230 KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32));
231 KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32));
232
233 char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray();
234 char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
235
236 ks.setKeyEntry(ENTRY_ALIAS, pair.getPrivate(), entryPassword, new Certificate[]{cert});
237 try (OutputStream out = Files.newOutputStream(path, StandardOpenOption.CREATE)) {
238 ks.store(out, storePassword);
239 }
240 }
241 return path;
242 }
243
244 /**
245 * Loads the JOSM keystore.
246 * @return the (initialized) JOSM keystore
247 * @throws IOException if an I/O error occurs
248 * @throws GeneralSecurityException if a security error occurs
249 * @since 7343
250 */
251 public static KeyStore loadJosmKeystore() throws IOException, GeneralSecurityException {
252 try (InputStream in = Files.newInputStream(setupJosmKeystore())) {
253 KeyStore ks = KeyStore.getInstance("JKS");
254 ks.load(in, KEYSTORE_PASSWORD.get().toCharArray());
255
256 if (Logging.isDebugEnabled()) {
257 for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
258 Logging.debug("Alias in JOSM keystore: {0}", aliases.nextElement());
259 }
260 }
261 return ks;
262 }
263 }
264
265 /**
266 * Initializes the TLS basics.
267 * @throws IOException if an I/O error occurs
268 * @throws GeneralSecurityException if a security error occurs
269 */
270 private void initialize() throws IOException, GeneralSecurityException {
271 KeyStore ks = loadJosmKeystore();
272
273 KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
274 kmf.init(ks, KEYENTRY_PASSWORD.get().toCharArray());
275
276 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
277 tmf.init(ks);
278
279 sslContext = SSLContext.getInstance("TLS");
280 sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
281
282 if (Logging.isTraceEnabled()) {
283 Logging.trace("SSL Context protocol: {0}", sslContext.getProtocol());
284 Logging.trace("SSL Context provider: {0}", sslContext.getProvider());
285 }
286
287 setupPlatform(ks);
288 }
289
290 /**
291 * Setup the platform-dependant certificate stuff.
292 * @param josmKs The JOSM keystore, containing localhost certificate and private key.
293 * @return {@code true} if something has changed as a result of the call (certificate installation, etc.)
294 * @throws KeyStoreException if the keystore has not been initialized (loaded)
295 * @throws NoSuchAlgorithmException in case of error
296 * @throws CertificateException in case of error
297 * @throws IOException in case of error
298 * @since 7343
299 */
300 public static boolean setupPlatform(KeyStore josmKs) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
301 Enumeration<String> aliases = josmKs.aliases();
302 if (aliases.hasMoreElements()) {
303 return Main.platform.setupHttpsCertificate(ENTRY_ALIAS,
304 new KeyStore.TrustedCertificateEntry(josmKs.getCertificate(aliases.nextElement())));
305 }
306 return false;
307 }
308
309 /**
310 * Starts or restarts the HTTPS server
311 */
312 public static void restartRemoteControlHttpsServer() {
313 stopRemoteControlHttpsServer();
314 if (RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get()) {
315 int port = Config.getPref().getInt("remote.control.https.port", HTTPS_PORT);
316 try {
317 instance4 = new RemoteControlHttpsServer(port, false);
318 instance4.start();
319 } catch (IOException | GeneralSecurityException ex) {
320 Logging.debug(ex);
321 Logging.warn(marktr("Cannot start IPv4 remotecontrol https server on port {0}: {1}"),
322 Integer.toString(port), ex.getLocalizedMessage());
323 }
324 try {
325 instance6 = new RemoteControlHttpsServer(port, true);
326 instance6.start();
327 } catch (IOException | GeneralSecurityException ex) {
328 /* only show error when we also have no IPv4 */
329 if (instance4 == null) {
330 Logging.debug(ex);
331 Logging.warn(marktr("Cannot start IPv6 remotecontrol https server on port {0}: {1}"),
332 Integer.toString(port), ex.getLocalizedMessage());
333 }
334 }
335 }
336 }
337
338 /**
339 * Stops the HTTPS server
340 */
341 public static void stopRemoteControlHttpsServer() {
342 if (instance4 != null) {
343 try {
344 instance4.stopServer();
345 } catch (IOException ioe) {
346 Logging.error(ioe);
347 }
348 instance4 = null;
349 }
350 if (instance6 != null) {
351 try {
352 instance6.stopServer();
353 } catch (IOException ioe) {
354 Logging.error(ioe);
355 }
356 instance6 = null;
357 }
358 }
359
360 /**
361 * Constructs a new {@code RemoteControlHttpsServer}.
362 * @param port The port this server will listen on
363 * @param ipv6 Whether IPv6 or IPv4 server should be started
364 * @throws IOException when connection errors
365 * @throws GeneralSecurityException in case of SSL setup errors
366 * @since 8339
367 */
368 public RemoteControlHttpsServer(int port, boolean ipv6) throws IOException, GeneralSecurityException {
369 super("RemoteControl HTTPS Server");
370 this.setDaemon(true);
371
372 initialize();
373
374 // Create SSL Server factory
375 SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
376 if (Logging.isTraceEnabled()) {
377 Logging.trace("SSL factory - Supported Cipher suites: {0}", Arrays.toString(factory.getSupportedCipherSuites()));
378 }
379
380 this.server = factory.createServerSocket(port, 1, ipv6 ?
381 RemoteControl.getInet6Address() : RemoteControl.getInet4Address());
382
383 if (Logging.isTraceEnabled() && server instanceof SSLServerSocket) {
384 SSLServerSocket sslServer = (SSLServerSocket) server;
385 Logging.trace("SSL server - Enabled Cipher suites: {0}", Arrays.toString(sslServer.getEnabledCipherSuites()));
386 Logging.trace("SSL server - Enabled Protocols: {0}", Arrays.toString(sslServer.getEnabledProtocols()));
387 Logging.trace("SSL server - Enable Session Creation: {0}", sslServer.getEnableSessionCreation());
388 Logging.trace("SSL server - Need Client Auth: {0}", sslServer.getNeedClientAuth());
389 Logging.trace("SSL server - Want Client Auth: {0}", sslServer.getWantClientAuth());
390 Logging.trace("SSL server - Use Client Mode: {0}", sslServer.getUseClientMode());
391 }
392 }
393
394 /**
395 * The main loop, spawns a {@link RequestProcessor} for each connection.
396 */
397 @Override
398 public void run() {
399 Logging.info(marktr("RemoteControl::Accepting secure remote connections on {0}:{1}"),
400 server.getInetAddress(), Integer.toString(server.getLocalPort()));
401 while (true) {
402 try {
403 @SuppressWarnings("resource")
404 Socket request = server.accept();
405 if (Logging.isTraceEnabled() && request instanceof SSLSocket) {
406 SSLSocket sslSocket = (SSLSocket) request;
407 Logging.trace("SSL socket - Enabled Cipher suites: {0}", Arrays.toString(sslSocket.getEnabledCipherSuites()));
408 Logging.trace("SSL socket - Enabled Protocols: {0}", Arrays.toString(sslSocket.getEnabledProtocols()));
409 Logging.trace("SSL socket - Enable Session Creation: {0}", sslSocket.getEnableSessionCreation());
410 Logging.trace("SSL socket - Need Client Auth: {0}", sslSocket.getNeedClientAuth());
411 Logging.trace("SSL socket - Want Client Auth: {0}", sslSocket.getWantClientAuth());
412 Logging.trace("SSL socket - Use Client Mode: {0}", sslSocket.getUseClientMode());
413 Logging.trace("SSL socket - Session: {0}", sslSocket.getSession());
414 }
415 RequestProcessor.processRequest(request);
416 } catch (SocketException e) {
417 if (!server.isClosed()) {
418 Logging.error(e);
419 }
420 } catch (IOException ioe) {
421 Logging.error(ioe);
422 }
423 }
424 }
425
426 /**
427 * Stops the HTTPS server.
428 *
429 * @throws IOException if any I/O error occurs
430 */
431 public void stopServer() throws IOException {
432 Logging.info(marktr("RemoteControl::Server {0}:{1} stopped."),
433 server.getInetAddress(), Integer.toString(server.getLocalPort()));
434 server.close();
435 }
436}
Note: See TracBrowser for help on using the repository browser.