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.io.OutputStream;
009import java.math.BigInteger;
010import java.net.ServerSocket;
011import java.net.Socket;
012import java.net.SocketException;
013import java.nio.file.Files;
014import java.nio.file.Path;
015import java.nio.file.Paths;
016import java.nio.file.StandardOpenOption;
017import java.security.GeneralSecurityException;
018import java.security.KeyPair;
019import java.security.KeyPairGenerator;
020import java.security.KeyStore;
021import java.security.KeyStoreException;
022import java.security.NoSuchAlgorithmException;
023import java.security.PrivateKey;
024import java.security.SecureRandom;
025import java.security.cert.Certificate;
026import java.security.cert.CertificateException;
027import java.security.cert.X509Certificate;
028import java.util.Arrays;
029import java.util.Date;
030import java.util.Enumeration;
031import java.util.Locale;
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;
043import org.openstreetmap.josm.spi.preferences.Config;
044import org.openstreetmap.josm.tools.Logging;
045
046import sun.security.util.ObjectIdentifier;
047import sun.security.x509.AlgorithmId;
048import sun.security.x509.BasicConstraintsExtension;
049import sun.security.x509.CertificateAlgorithmId;
050import sun.security.x509.CertificateExtensions;
051import sun.security.x509.CertificateSerialNumber;
052import sun.security.x509.CertificateValidity;
053import sun.security.x509.CertificateVersion;
054import sun.security.x509.CertificateX509Key;
055import sun.security.x509.DNSName;
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 final ServerSocket server;
077
078    /** The server instance for IPv4 */
079    private static volatile RemoteControlHttpsServer instance4;
080    /** The server instance for IPv6 */
081    private static volatile RemoteControlHttpsServer instance6;
082
083    /** SSL context information for connections */
084    private SSLContext sslContext;
085
086    /* the default port for HTTPS remote control */
087    private static final int HTTPS_PORT = 8112;
088
089    /**
090     * JOSM keystore file name.
091     * @since 7337
092     */
093    public static final String KEYSTORE_FILENAME = "josm.keystore";
094
095    /**
096     * Preference for keystore password (automatically generated by JOSM).
097     * @since 7335
098     */
099    public static final StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
100
101    /**
102     * Preference for certificate password (automatically generated by JOSM).
103     * @since 7335
104     */
105    public static final StringProperty KEYENTRY_PASSWORD = new StringProperty("remotecontrol.https.keyentry.password", "");
106
107    /**
108     * Unique alias used to store JOSM localhost entry, both in JOSM keystore and system/browser keystores.
109     * @since 7343
110     */
111    public static final String ENTRY_ALIAS = "josm_localhost";
112
113    /**
114     * Creates a GeneralNameInterface object from known types.
115     * @param t one of 4 known types
116     * @param v value
117     * @return which one
118     * @throws IOException if any I/O error occurs
119     */
120    private static GeneralNameInterface createGeneralNameInterface(String t, String v) throws IOException {
121        switch (t.toLowerCase(Locale.ENGLISH)) {
122            case "uri": return new URIName(v);
123            case "dns": return new DNSName(v);
124            case "ip": return new IPAddressName(v);
125            default: return new OIDName(v);
126        }
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 * 86_400_000L);
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        info.set(X509CertInfo.SUBJECT, owner);
152        info.set(X509CertInfo.ISSUER, owner);
153
154        info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic()));
155        info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
156        AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
157        info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo));
158
159        CertificateExtensions ext = new CertificateExtensions();
160        // Critical: Not CA, max path len 0
161        ext.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(Boolean.TRUE, false, 0));
162        // Critical: only allow TLS ("serverAuth" = 1.3.6.1.5.5.7.3.1)
163        ext.set(ExtendedKeyUsageExtension.NAME, new ExtendedKeyUsageExtension(Boolean.TRUE,
164                new Vector<>(Arrays.asList(new ObjectIdentifier("1.3.6.1.5.5.7.3.1")))));
165
166        if (san != null) {
167            int colonpos;
168            String[] ps = san.split(",");
169            GeneralNames gnames = new GeneralNames();
170            for (String item: ps) {
171                colonpos = item.indexOf(':');
172                if (colonpos < 0) {
173                    throw new IllegalArgumentException("Illegal item " + item + " in " + san);
174                }
175                String t = item.substring(0, colonpos);
176                String v = item.substring(colonpos+1);
177                gnames.add(new GeneralName(createGeneralNameInterface(t, v)));
178            }
179            // Non critical
180            ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(Boolean.FALSE, gnames));
181        }
182
183        info.set(X509CertInfo.EXTENSIONS, ext);
184
185        // Sign the cert to identify the algorithm that's used.
186        PrivateKey privkey = pair.getPrivate();
187        X509CertImpl cert = new X509CertImpl(info);
188        cert.sign(privkey, algorithm);
189
190        // Update the algorithm, and resign.
191        algo = (AlgorithmId) cert.get(X509CertImpl.SIG_ALG);
192        info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo);
193        cert = new X509CertImpl(info);
194        cert.sign(privkey, algorithm);
195        return cert;
196    }
197
198    /**
199     * Setup the JOSM internal keystore, used to store HTTPS certificate and private key.
200     * @return Path to the (initialized) JOSM keystore
201     * @throws IOException if an I/O error occurs
202     * @throws GeneralSecurityException if a security error occurs
203     * @since 7343
204     */
205    public static Path setupJosmKeystore() throws IOException, GeneralSecurityException {
206
207        Path dir = Paths.get(RemoteControl.getRemoteControlDir());
208        Path path = dir.resolve(KEYSTORE_FILENAME);
209        Files.createDirectories(dir);
210
211        if (!path.toFile().exists()) {
212            Logging.debug("No keystore found, creating a new one");
213
214            // Create new keystore like previous one generated with JDK keytool as follows:
215            // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap"
216            // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825
217
218            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
219            generator.initialize(2048);
220            KeyPair pair = generator.generateKeyPair();
221
222            X509Certificate cert = generateCertificate("CN=localhost, OU=JOSM, O=OpenStreetMap", pair, 1825, "SHA256withRSA",
223                    "dns:localhost,ip:127.0.0.1,ip:::1,uri:https://127.0.0.1:"+HTTPS_PORT+",uri:https://::1:"+HTTPS_PORT);
224
225            KeyStore ks = KeyStore.getInstance("JKS");
226            ks.load(null, null);
227
228            // Generate new passwords. See https://stackoverflow.com/a/41156/2257172
229            SecureRandom random = new SecureRandom();
230            KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32));
231            KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32));
232
233            char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray();
234            char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
235
236            ks.setKeyEntry(ENTRY_ALIAS, pair.getPrivate(), entryPassword, new Certificate[]{cert});
237            try (OutputStream out = Files.newOutputStream(path, StandardOpenOption.CREATE)) {
238                ks.store(out, storePassword);
239            }
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 (Logging.isDebugEnabled()) {
257                for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
258                    Logging.debug("Alias in JOSM keystore: {0}", 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 (Logging.isTraceEnabled()) {
283            Logging.trace("SSL Context protocol: {0}", sslContext.getProtocol());
284            Logging.trace("SSL Context provider: {0}", 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 = Config.getPref().getInt("remote.control.https.port", HTTPS_PORT);
316            try {
317                instance4 = new RemoteControlHttpsServer(port, false);
318                instance4.start();
319            } catch (IOException | GeneralSecurityException ex) {
320                Logging.debug(ex);
321                Logging.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                    Logging.debug(ex);
331                    Logging.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                Logging.error(ioe);
347            }
348            instance4 = null;
349        }
350        if (instance6 != null) {
351            try {
352                instance6.stopServer();
353            } catch (IOException ioe) {
354                Logging.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 (Logging.isTraceEnabled()) {
377            Logging.trace("SSL factory - Supported Cipher suites: {0}", Arrays.toString(factory.getSupportedCipherSuites()));
378        }
379
380        this.server = factory.createServerSocket(port, 1, ipv6 ?
381            RemoteControl.getInet6Address() : RemoteControl.getInet4Address());
382
383        if (Logging.isTraceEnabled() && server instanceof SSLServerSocket) {
384            SSLServerSocket sslServer = (SSLServerSocket) server;
385            Logging.trace("SSL server - Enabled Cipher suites: {0}", Arrays.toString(sslServer.getEnabledCipherSuites()));
386            Logging.trace("SSL server - Enabled Protocols: {0}", Arrays.toString(sslServer.getEnabledProtocols()));
387            Logging.trace("SSL server - Enable Session Creation: {0}", sslServer.getEnableSessionCreation());
388            Logging.trace("SSL server - Need Client Auth: {0}", sslServer.getNeedClientAuth());
389            Logging.trace("SSL server - Want Client Auth: {0}", sslServer.getWantClientAuth());
390            Logging.trace("SSL server - Use Client Mode: {0}", sslServer.getUseClientMode());
391        }
392    }
393
394    /**
395     * The main loop, spawns a {@link RequestProcessor} for each connection.
396     */
397    @Override
398    public void run() {
399        Logging.info(marktr("RemoteControl::Accepting secure remote connections on {0}:{1}"),
400                server.getInetAddress(), Integer.toString(server.getLocalPort()));
401        while (true) {
402            try {
403                @SuppressWarnings("resource")
404                Socket request = server.accept();
405                if (Logging.isTraceEnabled() && request instanceof SSLSocket) {
406                    SSLSocket sslSocket = (SSLSocket) request;
407                    Logging.trace("SSL socket - Enabled Cipher suites: {0}", Arrays.toString(sslSocket.getEnabledCipherSuites()));
408                    Logging.trace("SSL socket - Enabled Protocols: {0}", Arrays.toString(sslSocket.getEnabledProtocols()));
409                    Logging.trace("SSL socket - Enable Session Creation: {0}", sslSocket.getEnableSessionCreation());
410                    Logging.trace("SSL socket - Need Client Auth: {0}", sslSocket.getNeedClientAuth());
411                    Logging.trace("SSL socket - Want Client Auth: {0}", sslSocket.getWantClientAuth());
412                    Logging.trace("SSL socket - Use Client Mode: {0}", sslSocket.getUseClientMode());
413                    Logging.trace("SSL socket - Session: {0}", sslSocket.getSession());
414                }
415                RequestProcessor.processRequest(request);
416            } catch (SocketException e) {
417                if (!server.isClosed()) {
418                    Logging.error(e);
419                }
420            } catch (IOException ioe) {
421                Logging.error(ioe);
422            }
423        }
424    }
425
426    /**
427     * Stops the HTTPS server.
428     *
429     * @throws IOException if any I/O error occurs
430     */
431    public void stopServer() throws IOException {
432        Logging.info(marktr("RemoteControl::Server {0}:{1} stopped."),
433        server.getInetAddress(), Integer.toString(server.getLocalPort()));
434        server.close();
435    }
436}