From bd56bf854440d2d64a07f15be82a020b735ae1b2 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Fri, 19 Jul 2024 13:07:46 -0600 Subject: [PATCH] JNI: add example threaded client/server applications, client does session resumption with get/setSession() --- examples/README.md | 27 ++ examples/SimpleThreadedClient.java | 489 +++++++++++++++++++++++++++++ examples/SimpleThreadedClient.sh | 6 + examples/SimpleThreadedServer.java | 262 ++++++++++++++++ examples/SimpleThreadedServer.sh | 6 + 5 files changed, 790 insertions(+) create mode 100644 examples/SimpleThreadedClient.java create mode 100755 examples/SimpleThreadedClient.sh create mode 100644 examples/SimpleThreadedServer.java create mode 100755 examples/SimpleThreadedServer.sh diff --git a/examples/README.md b/examples/README.md index 8e9e0b3..3572a7c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 +$ ./examples/SimpleThreadedServer.sh +$ ./examples/SimpleThreadedClient.sh -n +``` + +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 ` 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 diff --git a/examples/SimpleThreadedClient.java b/examples/SimpleThreadedClient.java new file mode 100644 index 0000000..46a07b1 --- /dev/null +++ b/examples/SimpleThreadedClient.java @@ -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 store = + new SessionStore<>(defaultCacheSize); + private static final Object storeLock = new Object(); + + /** + * Inner SessionStore class, used for client session store. + */ + private class SessionStore extends LinkedHashMap { + /* 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 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 clientList = null; + ExecutorService executor = null; + + closedLatch = new CountDownLatch(numConnections); + clientList = new ArrayList(); + + /* 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 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 \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(); + } + } +} + + diff --git a/examples/SimpleThreadedClient.sh b/examples/SimpleThreadedClient.sh new file mode 100755 index 0000000..770cbd3 --- /dev/null +++ b/examples/SimpleThreadedClient.sh @@ -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 $@ + diff --git a/examples/SimpleThreadedServer.java b/examples/SimpleThreadedServer.java new file mode 100644 index 0000000..fdb5041 --- /dev/null +++ b/examples/SimpleThreadedServer.java @@ -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 */ + diff --git a/examples/SimpleThreadedServer.sh b/examples/SimpleThreadedServer.sh new file mode 100755 index 0000000..3293fbf --- /dev/null +++ b/examples/SimpleThreadedServer.sh @@ -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 $@ +