Simple Java HTTPS server
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.
Although this question is really old, someone mentioned me this topic and asked if it could be simplified. Most of the answers demonstrate very well how to setup a simple https server with sun, but I want to provide an alternative which is hopefully a bit easier.
For this setup I am assuming you already have the keystore and truststore in place.
The rest endpoint:
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));
}
}
}
Server configuration:
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();
}
}
I need to add some disclaimer here... I use SSLFactory class from the Github - SSLContext-Kickstart library to easily construct a SSLContext. It is maintained by me. You don't need to use it as others have provided a way to construct it with just plain java.
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
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.