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