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