web-dev-qa-db-fra.com

Simple Java serveur HTTPS

J'ai besoin de configurer un serveur HTTPS vraiment léger pour une application Java. C'est un simulateur qui est utilisé dans nos laboratoires de développement pour simuler les connexions HTTPS acceptées par un équipement dans la nature. Parce que c'est purement un outil de développement léger et n'est pas utilisé du tout en production, je suis très heureux de contourner les certifications et autant de négociations que possible.

Je prévois d'utiliser la classe HttpsServer dans Java 6 SE mais j'ai du mal à le faire fonctionner. En tant que client de test, j'utilise wget depuis la ligne de commande cygwin (wget https://[address]:[port]) mais wget signale qu'il était "Impossible d'établir une connexion SSL".

Si je lance wget avec le -d L'option de débogage me dit "La prise de contact SSL a échoué".

J'ai passé 30 minutes à googler cela et tout semble simplement renvoyer à la documentation Java 6 assez inutile qui décrit les méthodes mais ne parle pas réellement de la façon de faire parler de la chose sacrément ou fournir un exemple de code.

Quelqu'un peut-il me pousser dans la bonne direction?

42
Andrew

Ce que j'ai finalement utilisé était le suivant:

try {
    // Set up the socket address
    InetSocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(), config.getHttpsPort());

    // Initialise the HTTPS server
    HttpsServer httpsServer = HttpsServer.create(address, 0);
    SSLContext sslContext = SSLContext.getInstance("TLS");

    // Initialise the keystore
    char[] password = "simulator".toCharArray();
    KeyStore ks = KeyStore.getInstance("JKS");
    FileInputStream fis = new FileInputStream("lig.keystore");
    ks.load(fis, password);

    // Set up the key manager factory
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(ks, password);

    // Set up the trust manager factory
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
    tmf.init(ks);

    // Set up the HTTPS context and parameters
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
    httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
        public void configure(HttpsParameters params) {
            try {
                // Initialise the SSL context
                SSLContext c = SSLContext.getDefault();
                SSLEngine engine = c.createSSLEngine();
                params.setNeedClientAuth(false);
                params.setCipherSuites(engine.getEnabledCipherSuites());
                params.setProtocols(engine.getEnabledProtocols());

                // Get the default parameters
                SSLParameters defaultSSLParameters = c.getDefaultSSLParameters();
                params.setSSLParameters(defaultSSLParameters);
            } catch (Exception ex) {
                ILogger log = new LoggerFactory().getLogger();
                log.exception(ex);
                log.error("Failed to create HTTPS port");
            }
        }
    });
    LigServer server = new LigServer(httpsServer);
    joinableThreadList.add(server.getJoinableThread());
} catch (Exception exception) {
    log.exception(exception);
    log.error("Failed to create HTTPS server on port " + config.getHttpsPort() + " of localhost");
}

Pour générer un fichier de clés:

$ keytool -genkeypair -keyalg RSA -alias self_signed -keypass simulator \
  -keystore lig.keystore -storepass simulator

Voir aussi ici .

Potentiellement storepass et keypass peuvent être différents, auquel cas le ks.load et kmf.init doit utiliser respectivement storepass et keypass.

43
Andrew

J'ai mis à jour votre réponse pour un serveur HTTPS (non basé sur un socket). Cela pourrait aider avec CSRF et AJAX appels.

import Java.io.*;
import Java.net.InetSocketAddress;
import Java.lang.*;
import Java.net.URL;
import com.Sun.net.httpserver.HttpsServer;
import Java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import com.Sun.net.httpserver.*;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;

import Java.io.InputStreamReader;
import Java.io.Reader;
import Java.net.URLConnection;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import Java.security.cert.X509Certificate;

import Java.net.InetAddress;
import com.Sun.net.httpserver.HttpExchange;
import com.Sun.net.httpserver.HttpHandler;
import com.Sun.net.httpserver.HttpServer;
import com.Sun.net.httpserver.HttpsExchange;

public class SimpleHTTPSServer {

    public static class MyHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange t) throws IOException {
            String response = "This is the response";
            HttpsExchange httpsExchange = (HttpsExchange) t;
            t.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
            t.sendResponseHeaders(200, response.getBytes().length);
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
    }

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {

        try {
            // setup the socket address
            InetSocketAddress address = new InetSocketAddress(8000);

            // initialise the HTTPS server
            HttpsServer httpsServer = HttpsServer.create(address, 0);
            SSLContext sslContext = SSLContext.getInstance("TLS");

            // initialise the keystore
            char[] password = "password".toCharArray();
            KeyStore ks = KeyStore.getInstance("JKS");
            FileInputStream fis = new FileInputStream("testkey.jks");
            ks.load(fis, password);

            // setup the key manager factory
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            kmf.init(ks, password);

            // setup the trust manager factory
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            tmf.init(ks);

            // setup the HTTPS context and parameters
            sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
            httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
                public void configure(HttpsParameters params) {
                    try {
                        // initialise the SSL context
                        SSLContext context = getSSLContext();
                        SSLEngine engine = context.createSSLEngine();
                        params.setNeedClientAuth(false);
                        params.setCipherSuites(engine.getEnabledCipherSuites());
                        params.setProtocols(engine.getEnabledProtocols());

                        // Set the SSL parameters
                        SSLParameters sslParameters = context.getSupportedSSLParameters();
                        params.setSSLParameters(sslParameters);

                    } catch (Exception ex) {
                        System.out.println("Failed to create HTTPS port");
                    }
                }
            });
            httpsServer.createContext("/test", new MyHandler());
            httpsServer.setExecutor(null); // creates a default executor
            httpsServer.start();

        } catch (Exception exception) {
            System.out.println("Failed to create HTTPS server on port " + 8000 + " of localhost");
            exception.printStackTrace();

        }
    }

}

Pour créer un certificat auto-signé:

keytool -genkeypair -keyalg RSA -alias selfsigned -keystore testkey.jks -storepass password -validity 360 -keysize 2048
15
krishnakumar sekar

Juste un rappel aux autres: com.Sun.net.httpserver.HttpsServer dans les solutions ci-dessus ne fait pas partie de la norme Java. Bien qu'il soit fourni avec la JVM Oracle/OpenJDK, il n'est pas inclus dans toutes les JVM, donc cette ne fonctionnera pas hors de la boîte partout.

Il existe plusieurs serveurs HTTP légers que vous pouvez intégrer dans votre application qui prennent en charge HTTPS et s'exécuter sur n'importe quelle machine virtuelle Java.

L'un d'eux est JLHTTP - Le Java Lightweight HTTP Server qui est un petit serveur à un fichier (ou ~ 50K/35K jar) sans dépendances. keystore, SSLContext etc. est similaire à ce qui précède, car il repose également sur l'implémentation JSSE standard, ou vous pouvez spécifier les propriétés système standard pour configurer SSL. Vous pouvez voir la FAQ ou le code et sa documentation pour plus de détails.

Avertissement: je suis l'auteur de JLHTTP. Vous pouvez le vérifier par vous-même et déterminer s'il convient à vos besoins. J'espère que tu trouves cela utile :-)

2
amichair

Avec ServerSocket

Vous pouvez utiliser la classe qui HttpsServer est construite pour être encore plus légère: ServerSocket.

Filetage simple

Le programme suivant est un serveur à thread unique très simple qui écoute sur le port 8443. Les messages sont chiffrés avec TLS à l'aide des clés dans ./keystore.jks:

public static void main(String... args) {
    var address = new InetSocketAddress("0.0.0.0", 8443);

    startSingleThreaded(address);
}

public static void startSingleThreaded(InetSocketAddress address) {

    System.out.println("Start single-threaded server at " + address);

    try (var serverSocket = getServerSocket(address)) {

        var encoding = StandardCharsets.UTF_8;

        // This infinite loop is not CPU-intensive since method "accept" blocks
        // until a client has made a connection to the socket
        while (true) {
            try (var socket = serverSocket.accept();
                 // Use the socket to read the client's request
                 var reader = new BufferedReader(new InputStreamReader(
                         socket.getInputStream(), encoding.name()));
                 // Writing to the output stream and then closing it sends
                 // data to the client
                 var writer = new BufferedWriter(new OutputStreamWriter(
                         socket.getOutputStream(), encoding.name()))
            ) {
                getHeaderLines(reader).forEach(System.out::println);

                writer.write(getResponse(encoding));
                writer.flush();

            } catch (IOException e) {
                System.err.println("Exception while handling connection");
                e.printStackTrace();
            }
        }
    } catch (Exception e) {
        System.err.println("Could not create socket at " + address);
        e.printStackTrace();
    }
}

private static ServerSocket getServerSocket(InetSocketAddress address)
        throws Exception {

    // Backlog is the maximum number of pending connections on the socket,
    // 0 means that an implementation-specific default is used
    int backlog = 0;

    var keyStorePath = Path.of("./keystore.jks");
    char[] keyStorePassword = "pass_for_self_signed_cert".toCharArray();

    // Bind the socket to the given port and address
    var serverSocket = getSslContext(keyStorePath, keyStorePassword)
            .getServerSocketFactory()
            .createServerSocket(address.getPort(), backlog, address.getAddress());

    // We don't need the password anymore → Overwrite it
    Arrays.fill(keyStorePassword, '0');

    return serverSocket;
}

private static SSLContext getSslContext(Path keyStorePath, char[] keyStorePass)
        throws Exception {

    var keyStore = KeyStore.getInstance("JKS");
    keyStore.load(new FileInputStream(keyStorePath.toFile()), keyStorePass);

    var keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
    keyManagerFactory.init(keyStore, keyStorePass);

    var sslContext = SSLContext.getInstance("TLS");
    // Null means using default implementations for TrustManager and SecureRandom
    sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
    return sslContext;
}

private static String getResponse(Charset encoding) {
    var body = "The server says hi ????\r\n";
    var contentLength = body.getBytes(encoding).length;

    return "HTTP/1.1 200 OK\r\n" +
            String.format("Content-Length: %d\r\n", contentLength) +
            String.format("Content-Type: text/plain; charset=%s\r\n",
                    encoding.displayName()) +
            // An empty line marks the end of the response's header
            "\r\n" +
            body;
}

private static List<String> getHeaderLines(BufferedReader reader)
        throws IOException {
    var lines = new ArrayList<String>();
    var line = reader.readLine();
    // An empty line marks the end of the request's header
    while (!line.isEmpty()) {
        lines.add(line);
        line = reader.readLine();
    }
    return lines;
}

Multi-thread

Pour utiliser plusieurs threads pour le serveur, vous pouvez utiliser un pool de threads :

public static void startMultiThreaded(InetSocketAddress address) {

    try (var serverSocket = getServerSocket(address)) {

        System.out.println("Started multi-threaded server at " + address);

        // A cached thread pool with a limited number of threads
        var threadPool = newCachedThreadPool(8);

        var encoding = StandardCharsets.UTF_8;

        // This infinite loop is not CPU-intensive since method "accept" blocks
        // until a client has made a connection to the socket
        while (true) {
            try {
                var socket = serverSocket.accept();
                // Create a response to the request on a separate thread to
                // handle multiple requests simultaneously
                threadPool.submit(() -> {

                    try ( // Use the socket to read the client's request
                          var reader = new BufferedReader(new InputStreamReader(
                                  socket.getInputStream(), encoding.name()));
                          // Writing to the output stream and then closing it
                          // sends data to the client
                          var writer = new BufferedWriter(new OutputStreamWriter(
                                  socket.getOutputStream(), encoding.name()))
                    ) {
                        getHeaderLines(reader).forEach(System.out::println);
                        writer.write(getResponse(encoding));
                        writer.flush();
                        // We're done with the connection → Close the socket
                        socket.close();

                    } catch (Exception e) {
                        System.err.println("Exception while creating response");
                        e.printStackTrace();
                    }
                });
            } catch (IOException e) {
                System.err.println("Exception while handling connection");
                e.printStackTrace();
            }
        }
    } catch (Exception e) {
        System.err.println("Could not create socket at " + address);
        e.printStackTrace();
    }
}

private static ExecutorService newCachedThreadPool(int maximumNumberOfThreads) {
    return new ThreadPoolExecutor(0, maximumNumberOfThreads,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<>());
}

Créer un certificat

Utilisez keytool pour créer un certificat auto-signé (vous pouvez obtenir un certificat approprié auprès de Let's Encrypt gratuitement):

keytool -genkeypair -keyalg RSA -alias selfsigned -keystore keystore.jks \
        -storepass pass_for_self_signed_cert \
        -dname "CN=localhost, OU=Developers, O=Bull Bytes, L=Linz, C=AT"

Contacter le serveur

Après avoir démarré le serveur, connectez-vous avec curl :

curl -k https://localhost:8443

Cela va chercher un message sur le serveur:

Le serveur dit bonjour ????

Vérifiez quel protocole et quelle suite de chiffrement ont été établis par curl et votre serveur avec

curl -kv https://localhost:8443

En utilisant JDK 11 et curl 7.65.1, cela a produit

Connexion SSL à l'aide de TLSv1.3/TLS_AES_256_GCM_SHA384


Reportez-vous à Java Network Programming par Elliotte Rusty Harold pour plus d'informations sur le sujet.

0
Matthias Braun