001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.remotecontrol;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.OutputStream;
009import java.math.BigInteger;
010import java.net.ServerSocket;
011import java.net.Socket;
012import java.net.SocketException;
013import java.nio.file.Files;
014import java.nio.file.Path;
015import java.nio.file.Paths;
016import java.nio.file.StandardOpenOption;
017import java.security.GeneralSecurityException;
018import java.security.KeyPair;
019import java.security.KeyPairGenerator;
020import java.security.KeyStore;
021import java.security.KeyStoreException;
022import java.security.NoSuchAlgorithmException;
023import java.security.PrivateKey;
024import java.security.SecureRandom;
025import java.security.cert.Certificate;
026import java.security.cert.CertificateException;
027import java.security.cert.X509Certificate;
028import java.util.Arrays;
029import java.util.Date;
030import java.util.Enumeration;
031import java.util.Locale;
032import java.util.Vector;
033
034import javax.net.ssl.KeyManagerFactory;
035import javax.net.ssl.SSLContext;
036import javax.net.ssl.SSLServerSocket;
037import javax.net.ssl.SSLServerSocketFactory;
038import javax.net.ssl.SSLSocket;
039import javax.net.ssl.TrustManagerFactory;
040
041import org.openstreetmap.josm.Main;
042import org.openstreetmap.josm.data.preferences.StringProperty;
043
044import sun.security.util.ObjectIdentifier;
045import sun.security.x509.AlgorithmId;
046import sun.security.x509.BasicConstraintsExtension;
047import sun.security.x509.CertificateAlgorithmId;
048import sun.security.x509.CertificateExtensions;
049import sun.security.x509.CertificateSerialNumber;
050import sun.security.x509.CertificateValidity;
051import sun.security.x509.CertificateVersion;
052import sun.security.x509.CertificateX509Key;
053import sun.security.x509.ExtendedKeyUsageExtension;
054import sun.security.x509.GeneralName;
055import sun.security.x509.GeneralNameInterface;
056import sun.security.x509.GeneralNames;
057import sun.security.x509.IPAddressName;
058import sun.security.x509.OIDName;
059import sun.security.x509.SubjectAlternativeNameExtension;
060import sun.security.x509.URIName;
061import sun.security.x509.X500Name;
062import sun.security.x509.X509CertImpl;
063import sun.security.x509.X509CertInfo;
064
065/**
066 * Simple HTTPS server that spawns a {@link RequestProcessor} for every secure connection.
067 *
068 * @since 6941
069 */
070public class RemoteControlHttpsServer extends Thread {
071
072    /** The server socket */
073    private final ServerSocket server;
074
075    /** The server instance for IPv4 */
076    private static volatile RemoteControlHttpsServer instance4;
077    /** The server instance for IPv6 */
078    private static volatile RemoteControlHttpsServer instance6;
079
080    /** SSL context information for connections */
081    private SSLContext sslContext;
082
083    /* the default port for HTTPS remote control */
084    private static final int HTTPS_PORT = 8112;
085
086    /**
087     * JOSM keystore file name.
088     * @since 7337
089     */
090    public static final String KEYSTORE_FILENAME = "josm.keystore";
091
092    /**
093     * Preference for keystore password (automatically generated by JOSM).
094     * @since 7335
095     */
096    public static final StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
097
098    /**
099     * 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}