Simple Java HTTPS server
What I eventually used was this:
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");}
To generate a keystore:
$ keytool -genkeypair -keyalg RSA -alias self_signed -keypass simulator \ -keystore lig.keystore -storepass simulator
See also here.
Potentially storepass and keypass might be different, in which case the ks.load
and kmf.init
must use storepass and keypass, respectively.
I updated your answer for a HTTPS server (not socket-based). It might help with CSRF and AJAX calls.
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(); } }}
To create a self-signed certificate:
keytool -genkeypair -keyalg RSA -alias selfsigned -keystore testkey.jks -storepass password -validity 360 -keysize 2048
With ServerSocket
You can use the class that HttpsServer
is built around to be even more light-weight: ServerSocket
.
Single-threaded
The following program is a very simple, single-threaded server listening on port 8443. Messages are encrypted with TLS using the keys in ./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;}
Here's a project using this socket-based approach.
Multi-threaded
To use more than one thread for the server, you can employ a thread pool:
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<>());}
Create a certificate
Use the keytool
to create a self-signed certificate (you can get a proper certificate from Let's Encrypt for free):
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"
Contact the server
After starting the server, connect to it with curl:
curl -k https://localhost:8443
This will fetch a message from the server:
The server says hi 👋
Inspect which protocol and cipher suite were established by curl and your server with
curl -kv https://localhost:8443
Using JDK 13 and curl 7.66.0, this produced
SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
Refer to Java Network Programming by Elliotte Rusty Harold for more on the topic.