The Spiderwiz framework is based on the principle of total abstraction of the communication layer by confining all issues related to physical communication and protocols to the application configuration file. Currently Spiderwiz supports three types of communications – TCP/IP sockets, WebSockets and disk files (the latter used mainly for debugging). It is quite easy to build and use plugins that support other types, for example the Kafka plugin that was mentioned in Lesson 12. We will show you here how to do it.
As an exercise we will build a plugin that supports SSL TCP/IP communication. Actually we need two – one server that wraps SSLServerSocket and a client that wraps SSLSocket. Our implementation follows the explanation in the Web Service Security Tutorial and assumes that SSL certificates are installed as explained there.
Building a Spiderwiz communication server involves extending
ServerChannel and implementing few abstract
methods. Let’s go straight to the business with SslServerSocket
:
package org.spiderwiz.tutorial.lesson15.plugins; import java.io.IOException; import java.net.Socket; import java.util.Map; import javax.net.ServerSocketFactory; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import org.spiderwiz.core.Channel; import org.spiderwiz.core.Main; import org.spiderwiz.core.ServerChannel; import org.spiderwiz.zutils.ZUtilities; /** * Implementation of an SSL TCP/IP server plugin. */ public class SslServerSocket extends ServerChannel { private static final String TYPES[] = {"import", "producer", "consumer"}; private static final String NO_PORT = "No port number specified for %1$s server-%2$d"; private SSLServerSocket serverSocket = null; private int port; /** * Called by the framework to configure the server with the parameters defined in * "producer server-n" or "consumer server-n" configuration properties. The relevant parameters for an SSL server are: * port port number that the server needs to listen on. * ks a path to a file that contains the server key store. * pwd the key store private password. * @param configParams a map of key=value configuration parameters. * @param type type of server - 1:producer, 2:consumer. * @param n the n value of "producer server-n" or "consumer server-n" configuration properties. * @return true if and only if configuration is successful. */ @Override protected boolean configure(Map<String, String> configParams, int type, int n) { port = ZUtilities.parseInt(configParams.get("port")); if (port <= 0) { Main.getInstance().sendExceptionMail(null, String.format(NO_PORT, TYPES[type], n), null, true); return false; } String keystoreLocation = configParams.get("ks"); String keystorePassword = configParams.get("pwd"); if (keystoreLocation != null) System.setProperty("javax.net.ssl.keyStore", keystoreLocation); if (keystorePassword != null) System.setProperty("javax.net.ssl.keyStorePassword", keystorePassword); return true; } /** * Opens the server for arriving connection requests. * @throws IOException */ @Override protected void open() throws IOException { ServerSocketFactory ssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); serverSocket = (SSLServerSocket) ssf.createServerSocket(port); } /** * Listens for a connection to be made to this server and accepts it. * @return the server side of an SSL socket plugin. * @throws IOException */ @Override protected Channel accept() throws IOException { if (serverSocket == null) return null; Socket socket = serverSocket.accept(); if (socket == null) return null; SslSocket channel = new SslSocket(); channel.setSocket(socket); return channel; } /** * Closes the server. * @throws IOException */ @Override protected void close() throws IOException { if (serverSocket != null) serverSocket.close(); } /** * @return the server point identification. */ @Override public String getGateID() { return "SSL port " + port; } }
We start by implementing the abstract configure() method (line 36). Its first argument
is a mapping of keys to values that represent the parameters
defined in either “producer server-
n”
or “consumer server-
n” configuration
properties. The other arguments indicate whether this is a producer or a
consumer server and the n value of
the property key.
The configuration of an SSL server requires the following parameters:
port
the port number that the server listens on.ks
a path to the file that contains the Server Key Store.pwd
the Key Store password.
The method verifies that the port
parameter exists and specifies a valid number, then saves the
key store path and password as System properties.
The next is the abstract open() method implemented in line 56. It creates an SSLServerSocket object that listens to the specified port and saves it for later use.
The implementation of the abstract accept() method (line 67) calls ServerSocket.accept() that listens for a
connection to be made to this socket and accepts it. The received Socket object is wrapped by an SslSocket
object as explained below.
The implementation of the abstract close() method (line 83) closes the saved SSLServerSocket
.
The last method is an implementation of the abstract getGateID() method (line 92). It returns an identification of this server that is used by the logging system and SpiderAdmin.
The other part of the mission is to build the communication
plugin that wraps SSLSocket. This is done by extending Channel, implementing a few abstract methods and
overriding a few other methods if necessary. We do it with SslSocket
:
package org.spiderwiz.tutorial.lesson15.plugins; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.Map; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import org.spiderwiz.core.Channel; import org.spiderwiz.core.Main; import org.spiderwiz.zutils.ZUtilities; /** * Implementation of an SSL TCP/IP socket plugin. */ public class SslSocket extends Channel{ private static final String NO_TCP_PARAMS = "No IP address or port number specified for %1$s-%2$d"; private static final String TYPES[] = {"import", "producer", "consumer"}; private Socket socket = null; private String ip; private int port; /** * Called by the framework to configure the socket with the parameters defined in * "producer-n", "consumer-n" or "import-n" configuration properties. The relevant parameters for an SSL socket are: * ip the address of the server to connect to * port the port number to connect to * ks a path to a file that contains the client key store. * @param configParams a map of key=value configuration parameters. * @param type type of channel - 0: import, 1:producer, 2:consumer. * @param n the <em>n</em> value of <em>import-n</em>, <em>producer-n</em> or <em>consumer-n</em>. * @return true if and only if configuration is successful. */ @Override protected boolean configure(Map<String, String> configParams, int type, int n) { ip = configParams.get("ip"); port = ZUtilities.parseInt(configParams.get("port")); if (ip == null || port <= 0) { Main.getInstance().sendExceptionMail(null, String.format(NO_TCP_PARAMS, TYPES[type], n), null, true); return false; } socket = null; String truststoreLocation = configParams.get("ks"); if (truststoreLocation != null) System.setProperty("javax.net.ssl.trustStore", truststoreLocation); return true; } /** * Creates an SSL socket connection to the server and perform handshake. * @return true if and only if the operation is successful. * @throws UnknownHostException * @throws IOException */ @Override protected boolean open() throws UnknownHostException, IOException { // create a socket only if this is a client side socket. A server side socket already exists if (port != 0) { SSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault(); socket = f.createSocket(InetAddress.getByName(ip).getHostAddress(), port); ((SSLSocket)socket).startHandshake(); } return true; } /** * Disconnects by closing the socket. * @throws IOException */ @Override protected void close() throws IOException { if (socket != null) socket.close(); socket = null; } /** * @return the input stream of the connected socket * @throws IOException */ @Override protected InputStream getInputStream() throws IOException { return socket == null ? null : socket.getInputStream(); } /** * @return the output stream of the connected socket. * @throws IOException */ @Override protected OutputStream getOutputStream() throws IOException { return socket == null ? null : socket.getOutputStream(); } /** * A client socket should try to reconnect if disconnected while a server side socket shall not. * @return true if this is a server side socket. */ @Override protected boolean dontReconnect() { return port == 0; } /** * @return a string that represents the socket protocol and the address it is connected to in the form: * "ssl:<ip address>:<port number>". If this is a server side socket then the <port number> part is omitted. */ @Override public String getRemoteAddress() { return "ssl:" + ip + (port == 0 ? "" : ":" + port); } /** * Called by SslServerSocket when the server accepts a connection and creates a Socket object to handle it on the server * side. Attaches the Socket object to the current object and constructs the peer IP address. * @param socket the Socket object that shall be attached to this object. */ public void setSocket(Socket socket) { this.socket = socket; ip = socket.getRemoteSocketAddress().toString().replace("/", ""); int n = ip.indexOf(":"); if (n > 0) ip = ip.substring(0, n); port = 0; } }
Here we also need to implement a configure() method (line 29). The parameters for
a client connection, defined in producer-
n,
consumer-
n or import-
n configuration properties are:
ip
the IP address or server name to connect to.port
the port number to connect to.ks
a path to the file that contains the Client Key Store.
The method verifies that the ip
and port
parameters
exit and saves the client key store as a System property.
The override of the open() method (line 60) creates an SSLSocket object to handle the connection with
the configured server and uses it to start an SSL handshake on the connection. This is done
only if port
does not equal zero. It
does when the current object manages the server side of the connection, in
which case the socket is already opened as we will see below.
The override of the close() method (line 75) closes the managed socket.
The implementations of the abstract getInputStream() (line 86) and getOutputStream() (line 95) methods return an
input stream and an output stream respectively for the managed socket. Note
that you could implement these methods to return null
and override readLine() and writeLine() instead, but the former are
preferable because they support the built-in data compression at the physical level.
We override dontReconnect() (line 104) to return true
when port
equals zero, because this is an indication that the current
object manages a server-side socket that should not try to reconnect after
disconnection. The other case is a client-side socket that should try to
automatically reconnect to the specified port after reconnection. (Users can
configure the reattempts time interval using the reconnection seconds
configuration property).
We implement the abstract getRemoteAddress() (line 113) method to return
the address of the peer server, prefixed by the “ssl:
” protocol indication. If this object manages a client side
socket then the port number is appended to the address.
The last method is setSocket()
that is used by SslServerSocket
to
attach a server-side Socket
object to
the current Channel
object as we saw
above. The method also uses Socket.getRemoteSocketAddress() to set the IP
address of the client and sets port
to zero so that this will be identified as a server-side socket.
Our plugins are done. In order to test them, we implement SSL Hub – a command line Spiderwiz hub
that connects to My Hub of the
previous lessons and is also configured as a server using the new SslServerSocket
plugin (we could also
use My Hub as a hybrid server but
that would require us to rebuild it, while we prefer not to touch it). Here is SslHubMain
:
package org.spiderwiz.tutorial.lesson15.sslhub; import org.spiderwiz.tutorial.objectLib.ConsoleMain; /** * Main class for SSL Hub. */ public class SslHubMain extends ConsoleMain{ private static final String ROOT_DIRECTORY = "/tests/SslHub"; private static final String CONFIG_FILE_NAME = "sslhub.conf"; private static final String APP_NAME = "SSL Hub"; private static final String APP_VERSION = "Z15.01"; // Version Z15.01: SSL hub for Lesson 15 public SslHubMain() { super(ROOT_DIRECTORY, CONFIG_FILE_NAME, APP_NAME, APP_VERSION); } /** * Application entry point. Instantiate the class and initialize the instance. * * @param args the command line arguments. Not used in this application. */ public static void main(String[] args) { new SslHubMain().init(); } /** * @return an empty list as we produce nothing */ @Override protected String[] getProducedObjects() { return new String[]{}; } /** * @return an empty list as we produce nothing */ @Override protected String[] getConsumedObjects() { return new String[]{}; } /** * Process an input line - nothing to process in our case. * @param line The input line * @return true */ @Override protected boolean processConsoleLine(String line) { return true; } }
Just a barebones Spiderwiz console application. We define it as a hub in its configuration file:
[log folder]Logs [consumer server-1]class=org.spiderwiz.tutorial.lesson15.plugins.SslServerSocket;port=10002;ks=C:/Keys/ServerKeyStore.jks;pwd=Pass1word [producer-1]ip=localhost;port=10001 [hub mode]yes
A plugin server is configured through the same property as a
built-in server (consumer server-1
in
this case), except that the definition includes the class
parameter that specifies the fully qualified class name of
the plugin implementation. The other parameters are implementation dependent as
we saw above.
The same is done with a client plugin. Here is chat1.conf
:
[application name]Chat 1 [log folder]/tests/Chat1/Logs [backup folder]/tests/Chat1/Backup [history file]/tests/Chat1/history.dat [producer-1]class=org.spiderwiz.tutorial.lesson15.plugins.SslSocket;ip=localhost;port=10002;ks=C:/CA/ClientKeyStore.jks
In order for this to work, you need to rebuild both SSL Hub and the chat application with the plugin classes in the CLASSPATH.
That’s all. In the next lesson we will build a Mail plugin.