001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.remotecontrol;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.io.IOException;
008import java.io.InputStream;
009import java.math.BigInteger;
010import java.net.BindException;
011import java.net.InetAddress;
012import java.net.ServerSocket;
013import java.net.Socket;
014import java.net.SocketException;
015import java.nio.file.Files;
016import java.nio.file.Path;
017import java.nio.file.Paths;
018import java.nio.file.StandardOpenOption;
019import java.security.GeneralSecurityException;
020import java.security.KeyPair;
021import java.security.KeyPairGenerator;
022import java.security.KeyStore;
023import java.security.KeyStoreException;
024import java.security.NoSuchAlgorithmException;
025import java.security.PrivateKey;
026import java.security.SecureRandom;
027import java.security.cert.Certificate;
028import java.security.cert.CertificateException;
029import java.security.cert.X509Certificate;
030import java.util.Arrays;
031import java.util.Date;
032import java.util.Enumeration;
033import java.util.Vector;
034
035import javax.net.ssl.KeyManagerFactory;
036import javax.net.ssl.SSLContext;
037import javax.net.ssl.SSLServerSocket;
038import javax.net.ssl.SSLServerSocketFactory;
039import javax.net.ssl.SSLSocket;
040import javax.net.ssl.TrustManagerFactory;
041
042import org.openstreetmap.josm.Main;
043import org.openstreetmap.josm.data.preferences.StringProperty;
044
045import sun.security.util.ObjectIdentifier;
046import sun.security.x509.AlgorithmId;
047import sun.security.x509.BasicConstraintsExtension;
048import sun.security.x509.CertificateAlgorithmId;
049import sun.security.x509.CertificateExtensions;
050import sun.security.x509.CertificateIssuerName;
051import sun.security.x509.CertificateSerialNumber;
052import sun.security.x509.CertificateSubjectName;
053import sun.security.x509.CertificateValidity;
054import sun.security.x509.CertificateVersion;
055import sun.security.x509.CertificateX509Key;
056import sun.security.x509.ExtendedKeyUsageExtension;
057import sun.security.x509.GeneralName;
058import sun.security.x509.GeneralNameInterface;
059import sun.security.x509.GeneralNames;
060import sun.security.x509.IPAddressName;
061import sun.security.x509.OIDName;
062import sun.security.x509.SubjectAlternativeNameExtension;
063import sun.security.x509.URIName;
064import sun.security.x509.X500Name;
065import sun.security.x509.X509CertImpl;
066import sun.security.x509.X509CertInfo;
067
068/**
069 * Simple HTTPS server that spawns a {@link RequestProcessor} for every secure connection.
070 *
071 * @since 6941
072 */
073public class RemoteControlHttpsServer extends Thread {
074
075    /** The server socket */
076    private ServerSocket server;
077
078    private static RemoteControlHttpsServer instance;
079    private boolean initOK = false;
080    private SSLContext sslContext;
081
082    private static final int HTTPS_PORT = 8112;
083
084    /**
085     * JOSM keystore file name.
086     * @since 7337
087     */
088    public static final String KEYSTORE_FILENAME = "josm.keystore";
089
090    /**
091     * Preference for keystore password (automatically generated by JOSM).
092     * @since 7335
093     */
094    public static final StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
095
096    /**
097     * Preference for certificate password (automatically generated by JOSM).
098     * @since 7335
099     */
100    public static final StringProperty KEYENTRY_PASSWORD = new StringProperty("remotecontrol.https.keyentry.password", "");
101
102    /**
103     * Unique alias used to store JOSM localhost entry, both in JOSM keystore and system/browser keystores.
104     * @since 7343
105     */
106    public static final String ENTRY_ALIAS = "josm_localhost";
107
108    /**
109     * Creates a GeneralName object from known types.
110     * @param t one of 4 known types
111     * @param v value
112     * @return which one
113     * @throws IOException
114     */
115    private static GeneralName createGeneralName(String t, String v) throws IOException {
116        GeneralNameInterface gn;
117        switch (t.toLowerCase()) {
118            case "uri": gn = new URIName(v); break;
119            case "dns": gn = new DNSName(v); break;
120            case "ip": gn = new IPAddressName(v); break;
121            default: gn = new OIDName(v);
122        }
123        return new GeneralName(gn);
124    }
125
126    /**
127     * Create a self-signed X.509 Certificate.
128     * @param dn the X.509 Distinguished Name, eg "CN=localhost, OU=JOSM, O=OpenStreetMap"
129     * @param pair the KeyPair
130     * @param days how many days from now the Certificate is valid for
131     * @param algorithm the signing algorithm, eg "SHA256withRSA"
132     * @param san SubjectAlternativeName extension (optional)
133     */
134    private static X509Certificate generateCertificate(String dn, KeyPair pair, int days, String algorithm, String san) throws GeneralSecurityException, IOException {
135        PrivateKey privkey = pair.getPrivate();
136        X509CertInfo info = new X509CertInfo();
137        Date from = new Date();
138        Date to = new Date(from.getTime() + days * 86400000l);
139        CertificateValidity interval = new CertificateValidity(from, to);
140        BigInteger sn = new BigInteger(64, new SecureRandom());
141        X500Name owner = new X500Name(dn);
142
143        info.set(X509CertInfo.VALIDITY, interval);
144        info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn));
145
146        // Change of behaviour in JDK8:
147        // https://bugs.openjdk.java.net/browse/JDK-8040820
148        // https://bugs.openjdk.java.net/browse/JDK-7198416
149        String version = System.getProperty("java.version");
150        if (version == null || version.matches("^(1\\.)?[7].*")) {
151            // Java 7 code. To remove with Java 8 migration
152            info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(owner));
153            info.set(X509CertInfo.ISSUER, new CertificateIssuerName(owner));
154        } else {
155            // Java 8 and later code
156            info.set(X509CertInfo.SUBJECT, owner);
157            info.set(X509CertInfo.ISSUER, owner);
158        }
159
160        info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic()));
161        info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
162        AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
163        info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo));
164
165        CertificateExtensions ext = new CertificateExtensions();
166        // Critical: Not CA, max path len 0
167        ext.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(true, false, 0));
168        // Critical: only allow TLS ("serverAuth" = 1.3.6.1.5.5.7.3.1)
169        ext.set(ExtendedKeyUsageExtension.NAME, new ExtendedKeyUsageExtension(true,
170                new Vector<ObjectIdentifier>(Arrays.asList(new ObjectIdentifier("1.3.6.1.5.5.7.3.1")))));
171
172        if (san != null) {
173            int colonpos;
174            String[] ps = san.split(",");
175            GeneralNames gnames = new GeneralNames();
176            for(String item: ps) {
177                colonpos = item.indexOf(':');
178                if (colonpos < 0) {
179                    throw new IllegalArgumentException("Illegal item " + item + " in " + san);
180                }
181                String t = item.substring(0, colonpos);
182                String v = item.substring(colonpos+1);
183                gnames.add(createGeneralName(t, v));
184            }
185            // Non critical
186            ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(false, gnames));
187        }
188
189        info.set(X509CertInfo.EXTENSIONS, ext);
190
191        // Sign the cert to identify the algorithm that's used.
192        X509CertImpl cert = new X509CertImpl(info);
193        cert.sign(privkey, algorithm);
194
195        // Update the algorithm, and resign.
196        algo = (AlgorithmId)cert.get(X509CertImpl.SIG_ALG);
197        info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo);
198        cert = new X509CertImpl(info);
199        cert.sign(privkey, algorithm);
200        return cert;
201    }
202
203    /**
204     * Setup the JOSM internal keystore, used to store HTTPS certificate and private key.
205     * @return Path to the (initialized) JOSM keystore
206     * @throws IOException if an I/O error occurs
207     * @throws GeneralSecurityException if a security error occurs
208     * @since 7343
209     */
210    public static Path setupJosmKeystore() throws IOException, GeneralSecurityException {
211
212        char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray();
213        char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
214
215        Path dir = Paths.get(RemoteControl.getRemoteControlDir());
216        Path path = dir.resolve(KEYSTORE_FILENAME);
217        Files.createDirectories(dir);
218
219        if (!Files.exists(path)) {
220            Main.debug("No keystore found, creating a new one");
221
222            // Create new keystore like previous one generated with JDK keytool as follows:
223            // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap"
224            // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825
225
226            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
227            generator.initialize(2048);
228            KeyPair pair = generator.generateKeyPair();
229
230            X509Certificate cert = generateCertificate("CN=localhost, OU=JOSM, O=OpenStreetMap", pair, 1825, "SHA256withRSA",
231                    // see #10033#comment:20: All browsers respect "ip" in SAN, except IE which only understands DNS entries:
232                    // https://connect.microsoft.com/IE/feedback/details/814744/the-ie-doesnt-trust-a-san-certificate-when-connecting-to-ip-address
233                    "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);
234
235            KeyStore ks = KeyStore.getInstance("JKS");
236            ks.load(null, null);
237
238            // Generate new passwords. See https://stackoverflow.com/a/41156/2257172
239            SecureRandom random = new SecureRandom();
240            KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32));
241            KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32));
242
243            storePassword = KEYSTORE_PASSWORD.get().toCharArray();
244            entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
245
246            ks.setKeyEntry(ENTRY_ALIAS, pair.getPrivate(), entryPassword, new Certificate[]{cert});
247            ks.store(Files.newOutputStream(path, StandardOpenOption.CREATE), storePassword);
248        }
249        return path;
250    }
251
252    /**
253     * Loads the JOSM keystore.
254     * @return the (initialized) JOSM keystore
255     * @throws IOException if an I/O error occurs
256     * @throws GeneralSecurityException if a security error occurs
257     * @since 7343
258     */
259    public static KeyStore loadJosmKeystore() throws IOException, GeneralSecurityException {
260        try (InputStream in = Files.newInputStream(setupJosmKeystore())) {
261            KeyStore ks = KeyStore.getInstance("JKS");
262            ks.load(in, KEYSTORE_PASSWORD.get().toCharArray());
263
264            if (Main.isDebugEnabled()) {
265                for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
266                    Main.debug("Alias in JOSM keystore: "+aliases.nextElement());
267                }
268            }
269            return ks;
270        }
271    }
272
273    private void initialize() {
274        if (!initOK) {
275            try {
276                KeyStore ks = loadJosmKeystore();
277
278                KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
279                kmf.init(ks, KEYENTRY_PASSWORD.get().toCharArray());
280
281                TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
282                tmf.init(ks);
283
284                sslContext = SSLContext.getInstance("TLS");
285                sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
286
287                if (Main.isTraceEnabled()) {
288                    Main.trace("SSL Context protocol: " + sslContext.getProtocol());
289                    Main.trace("SSL Context provider: " + sslContext.getProvider());
290                }
291
292                setupPlatform(ks);
293
294                initOK = true;
295            } catch (IOException | GeneralSecurityException e) {
296                Main.error(e);
297            }
298        }
299    }
300
301    /**
302     * Setup the platform-dependant certificate stuff.
303     * @param josmKs The JOSM keystore, containing localhost certificate and private key.
304     * @return {@code true} if something has changed as a result of the call (certificate installation, etc.)
305     * @throws KeyStoreException if the keystore has not been initialized (loaded)
306     * @throws NoSuchAlgorithmException in case of error
307     * @throws CertificateException in case of error
308     * @throws IOException in case of error
309     * @since 7343
310     */
311    public static boolean setupPlatform(KeyStore josmKs) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
312        Enumeration<String> aliases = josmKs.aliases();
313        if (aliases.hasMoreElements()) {
314            return Main.platform.setupHttpsCertificate(ENTRY_ALIAS,
315                    new KeyStore.TrustedCertificateEntry(josmKs.getCertificate(aliases.nextElement())));
316        }
317        return false;
318    }
319
320    /**
321     * Starts or restarts the HTTPS server
322     */
323    public static void restartRemoteControlHttpsServer() {
324        int port = Main.pref.getInteger("remote.control.https.port", HTTPS_PORT);
325        try {
326            stopRemoteControlHttpsServer();
327
328            if (RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get()) {
329                instance = new RemoteControlHttpsServer(port);
330                if (instance.initOK) {
331                    instance.start();
332                }
333            }
334        } catch (BindException ex) {
335            Main.warn(marktr("Cannot start remotecontrol https server on port {0}: {1}"),
336                    Integer.toString(port), ex.getLocalizedMessage());
337        } catch (IOException ioe) {
338            Main.error(ioe);
339        } catch (NoSuchAlgorithmException e) {
340            Main.error(e);
341        }
342    }
343
344    /**
345     * Stops the HTTPS server
346     */
347    public static void stopRemoteControlHttpsServer() {
348        if (instance != null) {
349            try {
350                instance.stopServer();
351                instance = null;
352            } catch (IOException ioe) {
353                Main.error(ioe);
354            }
355        }
356    }
357
358    /**
359     * Constructs a new {@code RemoteControlHttpsServer}.
360     * @param port The port this server will listen on
361     * @throws IOException when connection errors
362     * @throws NoSuchAlgorithmException if the JVM does not support TLS (can not happen)
363     */
364    public RemoteControlHttpsServer(int port) throws IOException, NoSuchAlgorithmException {
365        super("RemoteControl HTTPS Server");
366        this.setDaemon(true);
367
368        initialize();
369
370        if (!initOK) {
371            Main.error(tr("Unable to initialize Remote Control HTTPS Server"));
372            return;
373        }
374
375        // Create SSL Server factory
376        SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
377        if (Main.isTraceEnabled()) {
378            Main.trace("SSL factory - Supported Cipher suites: "+Arrays.toString(factory.getSupportedCipherSuites()));
379        }
380
381        // Start the server socket with only 1 connection.
382        // Also make sure we only listen
383        // on the local interface so nobody from the outside can connect!
384        // NOTE: On a dual stack machine with old Windows OS this may not listen on both interfaces!
385        this.server = factory.createServerSocket(port, 1,
386            InetAddress.getByName(Main.pref.get("remote.control.host", "localhost")));
387
388        if (Main.isTraceEnabled() && server instanceof SSLServerSocket) {
389            SSLServerSocket sslServer = (SSLServerSocket) server;
390            Main.trace("SSL server - Enabled Cipher suites: "+Arrays.toString(sslServer.getEnabledCipherSuites()));
391            Main.trace("SSL server - Enabled Protocols: "+Arrays.toString(sslServer.getEnabledProtocols()));
392            Main.trace("SSL server - Enable Session Creation: "+sslServer.getEnableSessionCreation());
393            Main.trace("SSL server - Need Client Auth: "+sslServer.getNeedClientAuth());
394            Main.trace("SSL server - Want Client Auth: "+sslServer.getWantClientAuth());
395            Main.trace("SSL server - Use Client Mode: "+sslServer.getUseClientMode());
396        }
397    }
398
399    /**
400     * The main loop, spawns a {@link RequestProcessor} for each connection.
401     */
402    @Override
403    public void run() {
404        Main.info(marktr("RemoteControl::Accepting secure connections on port {0}"),
405             Integer.toString(server.getLocalPort()));
406        while (true) {
407            try {
408                @SuppressWarnings("resource")
409                Socket request = server.accept();
410                if (Main.isTraceEnabled() && request instanceof SSLSocket) {
411                    SSLSocket sslSocket = (SSLSocket) request;
412                    Main.trace("SSL socket - Enabled Cipher suites: "+Arrays.toString(sslSocket.getEnabledCipherSuites()));
413                    Main.trace("SSL socket - Enabled Protocols: "+Arrays.toString(sslSocket.getEnabledProtocols()));
414                    Main.trace("SSL socket - Enable Session Creation: "+sslSocket.getEnableSessionCreation());
415                    Main.trace("SSL socket - Need Client Auth: "+sslSocket.getNeedClientAuth());
416                    Main.trace("SSL socket - Want Client Auth: "+sslSocket.getWantClientAuth());
417                    Main.trace("SSL socket - Use Client Mode: "+sslSocket.getUseClientMode());
418                    Main.trace("SSL socket - Session: "+sslSocket.getSession());
419                }
420                RequestProcessor.processRequest(request);
421            } catch (SocketException se) {
422                if (!server.isClosed()) {
423                    Main.error(se);
424                }
425            } catch (IOException ioe) {
426                Main.error(ioe);
427            }
428        }
429    }
430
431    /**
432     * Stops the HTTPS server.
433     *
434     * @throws IOException if any I/O error occurs
435     */
436    public void stopServer() throws IOException {
437        if (server != null) {
438            server.close();
439            Main.info(marktr("RemoteControl::Server (https) stopped."));
440        }
441    }
442}