Простой Java HTTPS сервер

Мне нужно настроить действительно легкий HTTPS-сервер для приложения Java. Это симулятор, который используется в наших лабораториях разработки для имитации HTTPS-соединений, принимаемых частью оборудования в дикой природе. Поскольку это просто легкий инструмент разработки и он вообще не используется в производстве, я очень рад обойтись без сертификации и столько переговоров, сколько смогу.

Я планирую использовать HttpsServer класс в Java 6 SE, но я изо всех сил пытаюсь заставить его работать. В качестве тестового клиента я использую wget из командной строки cygwin (wget https://[address]:[port]) но wget сообщает, что не удалось установить SSL-соединение.

Если я бегу wget с -d опция для отладки говорит мне "SSL рукопожатие не удалось".

Я потратил 30 минут на поиски в Google, и все, кажется, просто указывает на довольно бесполезную документацию по Java6, которая описывает методы, но на самом деле не говорит о том, как говорить о чёртовой штуке или вообще предоставлять какой-либо пример кода.

Кто-нибудь может подтолкнуть меня в правильном направлении?

5 ответов

Решение

То, что я в итоге использовал, было так:

try
{
    // setup 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 );

    // 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 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" );
}

Чтобы создать хранилище ключей:

$ keytool -genkey -alias alias -keypass simulator \
  -keystore lig.keystore -storepass simulator

Смотрите также здесь.

Потенциально storepass и keypass могут отличаться, в этом случае ks.load а также kmf.init должны использовать storepass и keypass соответственно.

Я обновил ваш ответ для сервера https (не на основе сокетов), это может помочь с CSRF и AJAX Call

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 c = getSSLContext();
                        SSLEngine engine = c.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();

        }
    }

}

Создать самозаверяющий сертификат

keytool -genkey -keyalg RSA -alias selfsigned -keystore testkey.jks -storepass password -validity 360 -keysize 2048

С ServerSocket

Вы можете использовать класс, который HttpsServer построен вокруг, чтобы быть еще более легким: ServerSocket.

Однопоточный

Следующая программа представляет собой очень простой однопоточный сервер, прослушивающий порт 8443. Сообщения шифруются с помощью TLS с использованием ключей в ./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;
}

Вот проект, использующий этот подход на основе сокетов.

Многопоточный

Чтобы использовать более одного потока для сервера, вы можете использовать пул потоков:

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<>());
}

Создать сертификат

Использовать keytool для создания самозаверяющего сертификата (вы можете получить соответствующий сертификат в Let's Encrypt бесплатно):

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"

Связаться с сервером

После запуска сервера подключитесь к нему с помощью curl:

curl -k https://localhost:8443

Это получит сообщение с сервера:

Сервер говорит привет

Проверьте, какой протокол и набор шифров были установлены curl и вашим сервером с помощью

curl -kv https://localhost:8443

Используя JDK 13 и curl 7.66.0, это произвело

SSL-соединение с использованием TLSv1.3 / TLS_AES_256_GCM_SHA384


Обратитесь к Java Network Programming by Elliotte Rusty Harold для получения дополнительной информации по этой теме.

Хотя этот вопрос действительно старый, кто-то упомянул мне эту тему и спросил, можно ли ее упростить. Большинство ответов очень хорошо демонстрируют, как настроить простой https-сервер с солнцем, но я хочу предоставить альтернативу, которая, надеюсь, будет немного проще.

Для этой настройки я предполагаю, что у вас уже есть хранилище ключей и хранилище доверенных сертификатов.

Остальная конечная точка:

      import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class HelloWorldController implements HttpHandler {

    @Override
    public void handle(HttpExchange exchange) throws IOException {
        try (OutputStream responseBody = exchange.getResponseBody()) {

            exchange.getResponseHeaders().set("Content-Type", "text/plain");

            String payload = "Hello";
            exchange.sendResponseHeaders(200, payload.length());
            responseBody.write(payload.getBytes(StandardCharsets.UTF_8));
        }
    }

}

Конфигурация сервера:

      import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;
import nl.altindag.server.controller.HelloWorldController;
import nl.altindag.ssl.SSLFactory;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

public class App {

    public static void main(String[] args) throws IOException {
        SSLFactory sslFactory = SSLFactory.builder()
                .withIdentityMaterial("keystore.jks", "secret".toCharArray())
                .withTrustMaterial("truststore.jks", "secret".toCharArray())
                .build();

        InetSocketAddress socketAddress = new InetSocketAddress(8443);
        HttpsServer httpsServer = HttpsServer.create(socketAddress, 0);

        httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslFactory.getSslContext()) {
            @Override
            public void configure(HttpsParameters params) {
                params.setSSLParameters(sslFactory.getSslParameters());
            }
        });

        httpsServer.createContext("/api/hello", new HelloWorldController());
        httpsServer.setExecutor(Executors.newCachedThreadPool());
        httpsServer.start();
    }

}

Мне нужно добавить здесь заявление об отказе от ответственности... Я использую класс SSLFactory из библиотеки Github - SSLContext-Kickstart , чтобы легко создать SSLContext. Он поддерживается мной. Вам не нужно использовать его, так как другие предоставили способ его создания с помощью простого java.

Просто напоминание другим: com.sun.net.httpserver.HttpsServer в приведенных выше решениях не является частью стандарта Java, но является проприетарным и поставляется только с JVM Sun / Oracle, поэтому это не будет работать в любой другой среде выполнения Java.

Существует несколько облегченных HTTP-серверов, которые вы можете встроить в свое приложение, которые поддерживают HTTPS и работать на любой JVM.

Одним из них является JLHTTP - облегченный HTTP-сервер Java, представляющий собой крошечный однофайловый сервер (или ~50K/35K jar) без каких-либо зависимостей. Настройка хранилища ключей, SSLContext и т. Д. Аналогична описанной выше, поскольку она также зависит от стандартной реализации JSSE или вы можете указать стандартные системные свойства для настройки SSL. Вы можете увидеть FAQ или код и его документацию для деталей.

Отказ от ответственности: я автор JLHTTP. Вы можете проверить это сами и определить, соответствует ли это вашим потребностям. Я надеюсь, что вы найдете это полезным:-)

Другие вопросы по тегам