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 final 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<>(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        Path dir = Paths.get(RemoteControl.getRemoteControlDir());
219        Path path = dir.resolve(KEYSTORE_FILENAME);
220        Files.createDirectories(dir);
221
222        if (!Files.exists(path)) {
223            Main.debug("No keystore found, creating a new one");
224
225            // Create new keystore like previous one generated with JDK keytool as follows:
226            // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap"
227            // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825
228
229            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
230            generator.initialize(2048);
231            KeyPair pair = generator.generateKeyPair();
232
233            X509Certificate cert = generateCertificate("CN=localhost, OU=JOSM, O=OpenStreetMap", pair, 1825, "SHA256withRSA",
234                    // see #10033#comment:20: All browsers respect "ip" in SAN, except IE which only understands DNS entries:
235                    // CHECKSTYLE.OFF: LineLength
236                    // https://connect.microsoft.com/IE/feedback/details/814744/the-ie-doesnt-trust-a-san-certificate-when-connecting-to-ip-address
237                    // CHECKSTYLE.ON: LineLength
238                    "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);
239
240            KeyStore ks = KeyStore.getInstance("JKS");
241            ks.load(null, null);
242
243            // Generate new passwords. See https://stackoverflow.com/a/41156/2257172
244            SecureRandom random = new SecureRandom();
245            KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32));
246            KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32));
247
248            char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray();
249            char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
250
251            ks.setKeyEntry(ENTRY_ALIAS, pair.getPrivate(), entryPassword, new Certificate[]{cert});
252            ks.store(Files.newOutputStream(path, StandardOpenOption.CREATE), storePassword);
253        }
254        return path;
255    }
256
257    /**
258     * Loads the JOSM keystore.
259     * @return the (initialized) JOSM keystore
260     * @throws IOException if an I/O error occurs
261     * @throws GeneralSecurityException if a security error occurs
262     * @since 7343
263     */
264    public static KeyStore loadJosmKeystore() throws IOException, GeneralSecurityException {
265        try (InputStream in = Files.newInputStream(setupJosmKeystore())) {
266            KeyStore ks = KeyStore.getInstance("JKS");
267            ks.load(in, KEYSTORE_PASSWORD.get().toCharArray());
268
269            if (Main.isDebugEnabled()) {
270                for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
271                    Main.debug("Alias in JOSM keystore: "+aliases.nextElement());
272                }
273            }
274            return ks;
275        }
276    }
277
278    /**
279     * Initializes the TLS basics.
280     * @throws IOException if an I/O error occurs
281     * @throws GeneralSecurityException if a security error occurs
282     */
283    private void initialize() throws IOException, GeneralSecurityException {
284        KeyStore ks = loadJosmKeystore();
285
286        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
287        kmf.init(ks, KEYENTRY_PASSWORD.get().toCharArray());
288
289        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
290        tmf.init(ks);
291
292        sslContext = SSLContext.getInstance("TLS");
293        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
294
295        if (Main.isTraceEnabled()) {
296            Main.trace("SSL Context protocol: " + sslContext.getProtocol());
297            Main.trace("SSL Context provider: " + sslContext.getProvider());
298        }
299
300        setupPlatform(ks);
301    }
302
303    /**
304     * Setup the platform-dependant certificate stuff.
305     * @param josmKs The JOSM keystore, containing localhost certificate and private key.
306     * @return {@code true} if something has changed as a result of the call (certificate installation, etc.)
307     * @throws KeyStoreException if the keystore has not been initialized (loaded)
308     * @throws NoSuchAlgorithmException in case of error
309     * @throws CertificateException in case of error
310     * @throws IOException in case of error
311     * @since 7343
312     */
313    public static boolean setupPlatform(KeyStore josmKs) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
314        Enumeration<String> aliases = josmKs.aliases();
315        if (aliases.hasMoreElements()) {
316            return Main.platform.setupHttpsCertificate(ENTRY_ALIAS,
317                    new KeyStore.TrustedCertificateEntry(josmKs.getCertificate(aliases.nextElement())));
318        }
319        return false;
320    }
321
322    /**
323     * Starts or restarts the HTTPS server
324     */
325    public static void restartRemoteControlHttpsServer() {
326        stopRemoteControlHttpsServer();
327        if (RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get()) {
328            int port = Main.pref.getInteger("remote.control.https.port", HTTPS_PORT);
329            try {
330                instance4 = new RemoteControlHttpsServer(port, false);
331                instance4.start();
332            } catch (IOException | GeneralSecurityException ex) {
333                Main.warn(marktr("Cannot start IPv4 remotecontrol https server on port {0}: {1}"),
334                        Integer.toString(port), ex.getLocalizedMessage());
335            }
336            try {
337                instance6 = new RemoteControlHttpsServer(port, true);
338                instance6.start();
339            } catch (IOException | GeneralSecurityException ex) {
340                /* only show error when we also have no IPv4 */
341                if (instance4 == null) {
342                    Main.warn(marktr("Cannot start IPv6 remotecontrol https server on port {0}: {1}"),
343                        Integer.toString(port), ex.getLocalizedMessage());
344                }
345            }
346        }
347    }
348
349    /**
350     * Stops the HTTPS server
351     */
352    public static void stopRemoteControlHttpsServer() {
353        if (instance4 != null) {
354            try {
355                instance4.stopServer();
356            } catch (IOException ioe) {
357                Main.error(ioe);
358            }
359            instance4 = null;
360        }
361        if (instance6 != null) {
362            try {
363                instance6.stopServer();
364            } catch (IOException ioe) {
365                Main.error(ioe);
366            }
367            instance6 = null;
368        }
369    }
370
371    /**
372     * Constructs a new {@code RemoteControlHttpsServer}.
373     * @param port The port this server will listen on
374     * @param ipv6 Whether IPv6 or IPv4 server should be started
375     * @throws IOException when connection errors
376     * @throws NoSuchAlgorithmException if the JVM does not support TLS (can not happen)
377     * @throws GeneralSecurityException in case of SSL setup errors
378     * @since 8339
379     */
380    public RemoteControlHttpsServer(int port, boolean ipv6) throws IOException, NoSuchAlgorithmException, GeneralSecurityException {
381        super("RemoteControl HTTPS Server");
382        this.setDaemon(true);
383
384        initialize();
385
386        // Create SSL Server factory
387        SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
388        if (Main.isTraceEnabled()) {
389            Main.trace("SSL factory - Supported Cipher suites: "+Arrays.toString(factory.getSupportedCipherSuites()));
390        }
391
392        this.server = factory.createServerSocket(port, 1, ipv6 ?
393            RemoteControl.getInet6Address() : RemoteControl.getInet4Address());
394
395        if (Main.isTraceEnabled()) {
396            if (server instanceof SSLServerSocket) {
397                SSLServerSocket sslServer = (SSLServerSocket) server;
398                Main.trace("SSL server - Enabled Cipher suites: "+Arrays.toString(sslServer.getEnabledCipherSuites()));
399                Main.trace("SSL server - Enabled Protocols: "+Arrays.toString(sslServer.getEnabledProtocols()));
400                Main.trace("SSL server - Enable Session Creation: "+sslServer.getEnableSessionCreation());
401                Main.trace("SSL server - Need Client Auth: "+sslServer.getNeedClientAuth());
402                Main.trace("SSL server - Want Client Auth: "+sslServer.getWantClientAuth());
403                Main.trace("SSL server - Use Client Mode: "+sslServer.getUseClientMode());
404            }
405        }
406    }
407
408    /**
409     * The main loop, spawns a {@link RequestProcessor} for each connection.
410     */
411    @Override
412    public void run() {
413        Main.info(marktr("RemoteControl::Accepting secure remote connections on {0}:{1}"),
414                server.getInetAddress(), Integer.toString(server.getLocalPort()));
415        while (true) {
416            try {
417                @SuppressWarnings("resource")
418                Socket request = server.accept();
419                if (Main.isTraceEnabled() && request instanceof SSLSocket) {
420                    SSLSocket sslSocket = (SSLSocket) request;
421                    Main.trace("SSL socket - Enabled Cipher suites: "+Arrays.toString(sslSocket.getEnabledCipherSuites()));
422                    Main.trace("SSL socket - Enabled Protocols: "+Arrays.toString(sslSocket.getEnabledProtocols()));
423                    Main.trace("SSL socket - Enable Session Creation: "+sslSocket.getEnableSessionCreation());
424                    Main.trace("SSL socket - Need Client Auth: "+sslSocket.getNeedClientAuth());
425                    Main.trace("SSL socket - Want Client Auth: "+sslSocket.getWantClientAuth());
426                    Main.trace("SSL socket - Use Client Mode: "+sslSocket.getUseClientMode());
427                    Main.trace("SSL socket - Session: "+sslSocket.getSession());
428                }
429                RequestProcessor.processRequest(request);
430            } catch (SocketException se) {
431                if (!server.isClosed()) {
432                    Main.error(se);
433                }
434            } catch (IOException ioe) {
435                Main.error(ioe);
436            }
437        }
438    }
439
440    /**
441     * Stops the HTTPS server.
442     *
443     * @throws IOException if any I/O error occurs
444     */
445    public void stopServer() throws IOException {
446        Main.info(marktr("RemoteControl::Server {0}:{1} stopped."),
447        server.getInetAddress(), Integer.toString(server.getLocalPort()));
448        server.close();
449    }
450}