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