JNI: add example threaded client/server applications, client does session resumption with get/setSession()

pull/212/head
Chris Conlon 2024-07-19 13:07:46 -06:00
parent 3bca9810a8
commit bd56bf8544
5 changed files with 790 additions and 0 deletions

View File

@ -41,6 +41,33 @@ argument:
$ ./examples/server.sh --help
```
## wolfSSL JNI Example Simple Threaded Client and Server
Example client/server applications that use threads, which use
wolfSSL JNI (not JSSE):
**SimpleThreadedClient.java** - Example wolfSSL JNI threaded client \
**SimpleThreadedServer.java** - Example wolfSSL JNI threaded server
These examples can be run with the provided bash scripts:
```
$ cd <wolfssljni_root>
$ ./examples/SimpleThreadedServer.sh
$ ./examples/SimpleThreadedClient.sh -n <num_connections>
```
The `SimpleThreadedServer.java` starts at `localhost:11111` and waits for
client connections. When a client connection is received, it is handled in a
separate thread.
The `SimpleThreadedClient.java` makes concurrent client connections to a server
located at `localhost:11111`. Default number of client threads is **5**, but
can be changed using the `-n <num_connections>` command line argument. This
example implements a simple application-wide Java client cache where native
`WOLFSSL_SESSION` pointers are stored and used for session resumption where
possible. See code comments for further explanation.
## X509v3 Certificate Generation Example
An example is included which will generate self-signed and CA-signed

View File

@ -0,0 +1,489 @@
/* SimpleThreadedServer.java
*
* Copyright (C) 2006-2024 wolfSSL Inc.
*
* This file is part of wolfSSL.
*
* wolfSSL is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* wolfSSL is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
import java.io.IOException;
import java.net.Socket;
import java.net.ServerSocket;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Iterator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicIntegerArray;
import com.wolfssl.WolfSSL;
import com.wolfssl.WolfSSLContext;
import com.wolfssl.WolfSSLSession;
import com.wolfssl.WolfSSLJNIException;
/**
* This is a simple example of a launching multiple client threads
* connecting to the same server, while attemping to do session resumption
* where possible. Client threads are started and sleep a random
* value between 0 and 1 second so they are not all running at exactly the same
* time. This behavior more closely mimics real world application use.
*
* A application-wide static client session cache is implemented here as
* a LinkedHashMap. This is used to store WOLFSSL_SESSION pointer (long)
* values obtained from getSession(), and used again with setSession().
*
* This is meant to be a simple usage example, so there is not much
* customization currently from the command line. Certs and keys are hard
* coded to the values in class variables below.
*
* Client threads make SSL/TLS connections to localhost:11111, using the
* SSLv23_ClientMethod() during creation of the WolfSSLContext.
*
* This example has been designed to connect against the SimpleThreadedServer
* example in the same directory:
*
* cd wolfssljni
* ./examples/SimpleThreadedServer.sh
*
* ./examples/SimpleThreadedClient -n 10
*
*/
public class SimpleThreadedClient {
/* Cert and key info */
private String clientCert = "../certs/client-cert.pem";
private String clientKey = "../certs/client-key.pem";
private String caCert = "../certs/ca-cert.pem";
private String crlPemDir = "../certs/crl";
/* Server info */
int serverPort = 11111;
/* Number of client threads to start connecting to server,
* can be changed using the -n command line argument. */
int numConnections = 5;
/* Keep track of connection count that is resumed vs full */
final AtomicIntegerArray connectionsResumed = new AtomicIntegerArray(1);
final AtomicIntegerArray connectionsFull = new AtomicIntegerArray(1);
/* Client session store:
* Key: hash of host:port
* Value: pointer (long) to WOLFSSL_SESSION
* Default size: 33 sessions (defaultCacheSize)
*
* This is a Java session store to store WOLFSSL_SESSION pointers
* to be used with setSession(). Pointers will have been obtained
* with a call to getSession().
*
* Since the TLS 1.3 binder changes on each session connection, each entry
* should only be re-used by one thread/connection resumption at a time.
* We remove the session from the cache, try to resume it, and after
* a new successful connection put it back in the cache. This does mean
* that parallel concurrent client connections to the same host may not
* have all connections resume. Only the first thread to grab the session
* from the cache will potentially do a resumed session. Others will fall
* back to a new full handshake.
*
* Access to the session store should be synchronized on storeLock.
*/
private int defaultCacheSize = 33;
protected SessionStore<Integer, Long> store =
new SessionStore<>(defaultCacheSize);
private static final Object storeLock = new Object();
/**
* Inner SessionStore class, used for client session store.
*/
private class SessionStore<K, V> extends LinkedHashMap<K, V> {
/* User defined ID */
private static final long serialVersionUID = 1L;
/* Max LinkedHashMap size, before purging entries */
private final int maxSz;
/**
* Create new SessionStore.
* @param size max size of LinkedHashMap before oldest entry is
* overwritten
*/
protected SessionStore(int size) {
this.maxSz = size;
}
protected boolean removeEldestEntry(Map.Entry<K, V> oldest) {
return size() > maxSz;
}
}
/**
* Try to get a saved session (WOLFSSL_SESSION) from the client
* session cache. Sessions are keyed off the hash code of
* host+port. In production applications consder also keying off
* cipher suite and protocol version.
*
* @param port port number of peer being connected to
* @param host host of the peer being connected to
* @return an existing pointer (long) to a WOLFSSL_SESSION structure,
* removing it from the client cache so other threads do not try
* to resume it at the same time. Or, 0 if no session is found in
* cache.
*/
private synchronized long getSession(String host, int port) {
long sesPtr = 0;
String toHash = null;
if (host == null || host.isEmpty()) {
return 0;
}
synchronized (storeLock) {
toHash = host.concat(Integer.toString(port));
System.out.println("Entered getSession()\n" +
"| host = " + host + ", port = " + port + "\n" +
"| toHash = " + toHash + ", hashCode = " +
toHash.hashCode() + "\n" +
"| store = " + store);
if (toHash != null && store != null) {
Integer storeKey = new Integer(toHash.hashCode());
Long storeVal = this.store.get(storeKey);
if (storeVal != null) {
sesPtr = storeVal.longValue();
}
/* Delete entry from cache */
this.store.remove(storeKey);
}
if (sesPtr == 0) {
System.out.println("|-- SESSION NOT FOUND IN CACHE: 0");
} else {
System.out.println("|-- FOUND SESSION IN CACHE: " + sesPtr);
}
return sesPtr;
}
}
/**
* Store WOLFSSL_SESSION pointer (long) into client session cache, keying
* off hash code of host:port.
*
* Keep in mind that the WOLFSSL_SESSION pointer will need to be freed
* at some point with freeSession().
*
* @param port port number of peer connected to for this session
* @param host hots of peer connected to
*/
private synchronized void storeSession(String host, int port, long sesPtr) {
int hashCode = 0;
String toHash = null;
if (host == null || host.isEmpty() || sesPtr == 0) {
/* Invalid args, don't store */
return;
}
synchronized (storeLock) {
toHash = host.concat(Integer.toString(port));
hashCode = toHash.hashCode();
if (hashCode != 0) {
store.put(hashCode, sesPtr);
System.out.println("Entered storeSession()\n" +
"| stored session: " + sesPtr);
}
}
}
class ClientThread implements Runnable
{
private WolfSSLContext sslCtx = null;
private CountDownLatch closedLatch = null;
public ClientThread(WolfSSLContext sslCtx, CountDownLatch latch) {
this.sslCtx = sslCtx;
this.closedLatch = latch;
}
public void run() {
int ret = 0;
int err = 0;
int input = 0;
String host = "localhost";
WolfSSLSession ssl = null;
Socket sock = null;
byte[] back = new byte[80];
String msg = "hello from jni";
/* Pointer to native WOLFSSL_SESSION */
long session = 0;
try {
/* Introduce a random sleep per thread, so all client
* threads are started concurrently. Otherwise, one will get
* the cache entry and all others will not resume. Adding
* a random sleep makes this more realistic to what a real
* application would be encountering with staggered sessions
* across threads.
*
* Sleep here is random between zero and 1 second.
*/
Thread.sleep((long)(Math.random() * 1000));
/* Create new WolfSSLSession */
ssl = new WolfSSLSession(sslCtx);
/* Connect TCP Socket */
sock = new Socket(host, serverPort);
System.out.println("Connected to " +
sock.getInetAddress().getHostAddress() +
" on port " + sock.getPort() + "\n");
/* Pass Socket descriptor to wolfSSL */
ret = ssl.setFd(sock);
if (ret != WolfSSL.SSL_SUCCESS) {
throw new RuntimeException("Failed to set file descriptor");
}
/* Try to get session from session cache. We may not have a
* session in the cache yet if no connection has been made to
* this server, or another connection/thread already
* grabbed the client out of the cache. We need to remove the
* client session from the cache each time since the TLS 1.3
* binder changes between resumptions. */
session = getSession(host, serverPort);
if (session != 0) {
/* Restore saved WOLFSSL_SESSION, clear pointer after use */
ssl.setSession(session);
/* Free native WOLFSSL_SESSION memory */
ssl.freeSession(session);
session = 0;
}
do {
ret = ssl.connect();
err = ssl.getError(ret);
} while (ret != WolfSSL.SSL_SUCCESS &&
(err == WolfSSL.SSL_ERROR_WANT_READ ||
err == WolfSSL.SSL_ERROR_WANT_WRITE));
if (ret != WolfSSL.SSL_SUCCESS) {
err = ssl.getError(ret);
String errString = WolfSSL.getErrorString(err);
throw new RuntimeException(
"wolfSSL_connect failed. err = " + err +
", " + errString);
}
do {
ret = ssl.write(msg.getBytes(), msg.length());
err = ssl.getError(0);
} while (ret < 0 &&
(err == WolfSSL.SSL_ERROR_WANT_READ ||
err == WolfSSL.SSL_ERROR_WANT_WRITE));
/* Read response */
do {
input = ssl.read(back, back.length);
err = ssl.getError(0);
} while (input < 0 &&
(err == WolfSSL.SSL_ERROR_WANT_READ ||
err == WolfSSL.SSL_ERROR_WANT_WRITE));
if (input > 0) {
System.out.println("got back: " + new String(back));
} else {
throw new RuntimeException("read failed");
}
/* Did we resume a connection from the cache? */
if (ssl.sessionReused() == 1) {
System.out.println("Session resumed");
connectionsResumed.incrementAndGet(0);
} else {
System.out.println("New session was made, not resumed");
connectionsFull.incrementAndGet(0);
}
/* Get native WOLFSSL_SESSION and store into our local
* client cache for resumption attempts later */
session = ssl.getSession();
if (session == 0) {
System.out.println("Failed to get native WOLFSSL_SESSION");
} else {
storeSession(host, serverPort, session);
session = 0;
System.out.println("Saved native WOLFSSL_SESSION");
}
ssl.shutdownSSL();
ssl.freeSSL();
ssl = null;
sock.close();
sock = null;
closedLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sock != null) {
try {
sock.close();
} catch (IOException e) {
e.printStackTrace();
}
sock = null;
}
}
}
}
private void LaunchClientThreads()
throws Exception {
int ret = 0;
WolfSSLContext sslCtx = null;
CountDownLatch closedLatch = null;
List<ClientThread> clientList = null;
ExecutorService executor = null;
closedLatch = new CountDownLatch(numConnections);
clientList = new ArrayList<ClientThread>();
/* Create one WolfSSLContext to share accross all client threads */
sslCtx = new WolfSSLContext(WolfSSL.SSLv23_ClientMethod());
/* Load client certificate */
ret = sslCtx.useCertificateFile(clientCert,
WolfSSL.SSL_FILETYPE_PEM);
if (ret != WolfSSL.SSL_SUCCESS) {
throw new RuntimeException(
"failed to load client certificate!");
}
/* Load client private key */
ret = sslCtx.usePrivateKeyFile(clientKey,
WolfSSL.SSL_FILETYPE_PEM);
if (ret != WolfSSL.SSL_SUCCESS) {
throw new RuntimeException(
"failed to load client private key!");
}
/* Load CA certificate to verify server */
ret = sslCtx.loadVerifyLocations(caCert, null);
if (ret != WolfSSL.SSL_SUCCESS) {
throw new RuntimeException("failed to load CA certificates!");
}
/* Start client threads */
for (int i = 0; i < numConnections; i++) {
clientList.add(new ClientThread(sslCtx, closedLatch));
}
executor = Executors.newFixedThreadPool(clientList.size());
for (final ClientThread c : clientList) {
executor.execute(c);
}
/* Wait for client connections to finish */
closedLatch.await();
executor.shutdown();
/* Go through Java client session cache and free memory for
* native WOLFSSL_SESSION pointers */
synchronized (storeLock) {
Iterator<Integer> iterator = store.keySet().iterator();
while (iterator.hasNext()) {
Integer key = iterator.next();
Long ptr = store.get(key);
if (ptr != 0) {
WolfSSLSession.freeSession(ptr);
}
iterator.remove();
}
}
System.out.println("\nCompleted " + numConnections +
" client connections\n");
System.out.println("\tconnections resumed: " +
connectionsResumed.get(0));
System.out.println("\tconnections full handshake: " +
connectionsFull.get(0));
}
public void run(String[] args) throws Exception {
/* Read and process command line options from user */
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (arg.equals("-help")) {
printUsage();
return;
}
else if (arg.equals("-n")) {
if (args.length < i+2) {
printUsage();
return;
}
numConnections = Integer.parseInt(args[++i]);
}
}
connectionsResumed.set(0, 0);
connectionsFull.set(0, 0);
/* Setup and start client threads */
LaunchClientThreads();
}
private void printUsage() {
System.out.println("Threaded Client Example:");
System.out.println("-help\t\tHelp, print this usage");
System.out.println("-n <num>\tNumber of threads/connections");
}
public static void main(String[] args) {
try {
/* Load library */
WolfSSL.loadLibrary();
SimpleThreadedClient test = new SimpleThreadedClient();
test.run(args);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,6 @@
#!/bin/bash
cd ./examples/build
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../lib/:/usr/local/lib
java -classpath ../../lib/wolfssl.jar:./ -Dsun.boot.library.path=../../lib/ SimpleThreadedClient $@

View File

@ -0,0 +1,262 @@
/* SimpleThreadedServer.java
*
* Copyright (C) 2006-2024 wolfSSL Inc.
*
* This file is part of wolfSSL.
*
* wolfSSL is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* wolfSSL is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
import java.io.*;
import java.net.*;
import java.nio.*;
import java.util.*;
import com.wolfssl.WolfSSL;
import com.wolfssl.WolfSSLSession;
import com.wolfssl.WolfSSLContext;
import com.wolfssl.WolfSSLCertificate;
import com.wolfssl.WolfSSLException;
import com.wolfssl.WolfSSLJNIException;
/**
* Simple SSL/TLS server that uses wolfSSL JNI (not JSSE).
* The server listens for client connections at localhost:11111 and
* handles each client in a separate thread as they come in.
*
* This is meant to be a very simple example and does not currently offer
* much customization. It uses the hard-coded certs and keys found below,
* and creates the WolfSSLContext using SSLv23_ServerMethod().
*/
public class SimpleThreadedServer {
public static void main(String[] args) {
new SimpleThreadedServer().run(args);
}
public void run(String[] args) {
int ret = 0;
int serverPort = 11111;
Socket clientSocket = null;
ServerSocket serverSocket = null;
/* Cert and Key info */
String serverCert = "../certs/server-cert.pem";
String serverKey = "../certs/server-key.pem";
String caCert = "../certs/client-cert.pem";
String crlPemDir = "../certs/crl";
String dhParam = "../certs/dh2048.pem";
try {
/* Load JNI library */
WolfSSL.loadLibrary();
/* Init library */
WolfSSL sslLib = new WolfSSL();
/* Create context */
WolfSSLContext sslCtx = new WolfSSLContext(
WolfSSL.SSLv23_ServerMethod());
/* Load certificate/key files */
ret = sslCtx.useCertificateChainFile(serverCert);
if (ret != WolfSSL.SSL_SUCCESS) {
System.out.println("failed to load server certificate!");
System.exit(1);
}
ret = sslCtx.usePrivateKeyFile(serverKey,
WolfSSL.SSL_FILETYPE_PEM);
if (ret != WolfSSL.SSL_SUCCESS) {
System.out.println("failed to load server private key!");
System.exit(1);
}
/* Set verify callback */
ret = sslCtx.loadVerifyLocations(caCert, null);
if (ret != WolfSSL.SSL_SUCCESS) {
System.out.println("failed to load CA certificates!");
System.exit(1);
}
/* Create server socket */
serverSocket = new ServerSocket(serverPort);
System.out.println("Started server at " +
InetAddress.getLocalHost() + ", port " + serverPort);
/* Wait for new client connections, process each in new thread */
while (true) {
clientSocket = serverSocket.accept();
System.out.println("client connection received from " +
clientSocket.getInetAddress().getHostAddress() +
" at port " + clientSocket.getLocalPort() + "\n");
ClientHandler client = new ClientHandler(clientSocket, sslCtx);
client.start();
}
} catch (UnsatisfiedLinkError | WolfSSLException | IOException e) {
e.printStackTrace();
} finally {
try {
if (serverSocket != null) {
serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
} /* end run() */
class ClientHandler extends Thread
{
int ret = 0;
int insz = 0;
int err = 0;
Socket clientSocket;
WolfSSLContext sslCtx;
String msg = "I hear you fa shizzle, from Java!";
byte[] input = new byte[80];
public ClientHandler(Socket s, WolfSSLContext ctx) {
clientSocket = s;
sslCtx = ctx;
}
public void run() {
WolfSSLSession ssl = null;
DataOutputStream outstream = null;
DataInputStream instream = null;
try {
/* Get input and output streams */
outstream = new DataOutputStream(
clientSocket.getOutputStream());
instream = new DataInputStream(
clientSocket.getInputStream());
/* Create SSL object */
ssl = new WolfSSLSession(sslCtx);
/* Pass socket fd to wolfSSL */
ret = ssl.setFd(clientSocket);
if (ret != WolfSSL.SSL_SUCCESS) {
throw new RuntimeException("Failed to set file descriptor");
}
do {
ret = ssl.accept();
err = ssl.getError(ret);
} while (ret != WolfSSL.SSL_SUCCESS &&
(err == WolfSSL.SSL_ERROR_WANT_READ ||
err == WolfSSL.SSL_ERROR_WANT_WRITE));
if (ret != WolfSSL.SSL_SUCCESS) {
err = ssl.getError(ret);
String errString = WolfSSL.getErrorString(err);
throw new RuntimeException(
"wolfSSL_accept failed. err = " + err +
", " + errString);
}
/* Show peer info */
showPeer(ssl);
/* Read client response, and echo */
do {
insz = ssl.read(input, input.length);
err = ssl.getError(0);
} while (insz < 0 &&
(err == WolfSSL.SSL_ERROR_WANT_READ ||
err == WolfSSL.SSL_ERROR_WANT_WRITE));
if (input.length > 0) {
String cliMsg = new String(input, 0, insz);
System.out.println("client says: " + cliMsg);
} else {
throw new RuntimeException("read failed");
}
do {
ret = ssl.write(msg.getBytes(), msg.length());
err = ssl.getError(0);
} while (ret < 0 &&
(err == WolfSSL.SSL_ERROR_WANT_READ ||
err == WolfSSL.SSL_ERROR_WANT_WRITE));
if (ret != msg.length()) {
throw new RuntimeException("ssl.write() failed");
}
ssl.shutdownSSL();
} catch (WolfSSLException | IOException e) {
e.printStackTrace();
} finally {
if (ssl != null) {
try {
ssl.freeSSL();
} catch (WolfSSLJNIException e) {
e.printStackTrace();
}
}
}
}
}
void showPeer(WolfSSLSession ssl) {
String altname;
long peerCrtPtr = 0;
try {
System.out.println("TLS version is " + ssl.getVersion());
System.out.println("TLS cipher suite is " + ssl.cipherGetName());
peerCrtPtr = ssl.getPeerCertificate();
if (peerCrtPtr != 0) {
System.out.println(
"issuer : " + ssl.getPeerX509Issuer(peerCrtPtr));
System.out.println(
"subject : " + ssl.getPeerX509Subject(peerCrtPtr));
while((altname = ssl.getPeerX509AltName(peerCrtPtr)) != null) {
System.out.println("altname = " + altname);
}
}
} catch (WolfSSLJNIException e) {
e.printStackTrace();
} finally {
if (WolfSSL.getLibVersionHex() >= 0x05003000) {
if (peerCrtPtr != 0) {
WolfSSLCertificate.freeX509(peerCrtPtr);
}
}
}
}
} /* end Server */

View File

@ -0,0 +1,6 @@
#!/bin/bash
cd ./examples/build
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../lib/:/usr/local/lib
java -classpath ../../lib/wolfssl.jar:./ -Dsun.boot.library.path=../../lib/ SimpleThreadedServer $@