{"id":1006,"date":"2020-08-13T20:17:14","date_gmt":"2020-08-13T20:17:14","guid":{"rendered":"http:\/\/spiderwiz.org\/project\/?page_id=1006"},"modified":"2025-04-09T06:15:58","modified_gmt":"2025-04-09T06:15:58","slug":"lesson-15","status":"publish","type":"page","link":"https:\/\/spiderwiz.org\/project\/tutorial\/lesson-15\/","title":{"rendered":"Lesson 15: Writing a Communication Plugin"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The Spiderwiz framework is based on the principle of total\nabstraction of the communication layer by confining all issues related to\nphysical communication and protocols to the application configuration file.\nCurrently Spiderwiz supports three types of communications \u2013 TCP\/IP sockets,\nWebSockets and disk files (the latter used mainly for debugging). It is quite\neasy to build and use plugins that support other types, for example the <a href=\"https:\/\/kafka.apache.org\/\">Kafka<\/a>\nplugin that was mentioned in <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-12\/\">Lesson 12<\/a>. We will show you here how to do it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As an exercise we will build a plugin that supports SSL\nTCP\/IP communication. Actually we need two \u2013 one server that wraps <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/javax\/net\/ssl\/SSLServerSocket.html\">SSLServerSocket<\/a> and a client that wraps <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/javax\/net\/ssl\/SSLSocket.html\">SSLSocket<\/a>. Our implementation follows the\nexplanation in the <a href=\"https:\/\/sites.google.com\/site\/ddmwsst\/create-your-own-certificate-and-ca\/ssl-socket-communication\">Web Service Security Tutorial<\/a> and assumes that\nSSL certificates are installed as explained there.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Building a Spiderwiz communication server involves extending\n<a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/ServerChannel.html\">ServerChannel<\/a> and implementing few abstract\nmethods. Let\u2019s go straight to the business with <code>SslServerSocket<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson15.plugins;\n\nimport java.io.IOException;\nimport java.net.Socket;\nimport java.util.Map;\nimport javax.net.ServerSocketFactory;\nimport javax.net.ssl.SSLServerSocket;\nimport javax.net.ssl.SSLServerSocketFactory;\nimport org.spiderwiz.core.Channel;\nimport org.spiderwiz.core.Main;\nimport org.spiderwiz.core.ServerChannel;\nimport org.spiderwiz.zutils.ZUtilities;\n\n\/**\n * Implementation of an SSL TCP\/IP server plugin.\n *\/\npublic class SslServerSocket extends ServerChannel {\n    private static final String TYPES[] = {&quot;import&quot;, &quot;producer&quot;, &quot;consumer&quot;};\n    private static final String NO_PORT = &quot;No port number specified for %1$s server-%2$d&quot;;\n\n    private SSLServerSocket serverSocket = null;\n    private int port;\n\n    \/**\n     * Called by the framework to configure the server with the parameters defined in\n     * &quot;producer server-n&quot; or &quot;consumer server-n&quot; configuration properties. The relevant parameters for an SSL server are:\n     *      port    port number that the server needs to listen on.\n     *      ks      a path to a file that contains the server key store.\n     *      pwd     the key store private password.\n     * @param configParams      a map of key=value configuration parameters.\n     * @param type              type of server - 1:producer, 2:consumer.\n     * @param n                 the n value of &quot;producer server-n&quot; or &quot;consumer server-n&quot; configuration properties.\n     * @return true if and only if configuration is successful.\n     *\/\n    @Override\n    protected boolean configure(Map&lt;String, String&gt; configParams, int type, int n) {\n        port = ZUtilities.parseInt(configParams.get(&quot;port&quot;));\n        if (port &lt;= 0) {\n            Main.getInstance().sendExceptionMail(null, String.format(NO_PORT, TYPES[type], n), null, true);\n            return false;\n        }\n        String keystoreLocation = configParams.get(&quot;ks&quot;);\n        String keystorePassword = configParams.get(&quot;pwd&quot;);\n        if (keystoreLocation != null)\n            System.setProperty(&quot;javax.net.ssl.keyStore&quot;, keystoreLocation);\n        if (keystorePassword != null)\n            System.setProperty(&quot;javax.net.ssl.keyStorePassword&quot;, keystorePassword);\n        return true;\n    }\n    \n    \/**\n     * Opens the server for arriving connection requests.\n     * @throws IOException\n     *\/\n    @Override\n    protected void open() throws IOException {\n        ServerSocketFactory ssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();\n        serverSocket = (SSLServerSocket) ssf.createServerSocket(port);\n    }\n\n    \/**\n     * Listens for a connection to be made to this server and accepts it.\n     * @return the server side of an SSL socket plugin.\n     * @throws IOException\n     *\/\n    @Override\n    protected Channel accept() throws IOException {\n        if (serverSocket == null)\n            return null;\n        Socket socket = serverSocket.accept();\n        if (socket == null)\n            return null;\n        SslSocket channel = new SslSocket();\n        channel.setSocket(socket);\n        return channel;\n    }\n\n    \/**\n     * Closes the server.\n     * @throws IOException\n     *\/\n    @Override\n    protected void close() throws IOException {\n        if (serverSocket != null)\n            serverSocket.close();\n    }\n\n    \/**\n     * @return the server point identification.\n     *\/\n    @Override\n    public String getGateID() {\n        return &quot;SSL port &quot; + port;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We start by implementing the abstract <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/ServerChannel.html#configure(java.util.Map,int,int)\">configure()<\/a> method (line 36). Its first argument\nis a mapping of <em>keys<\/em> to <em>values<\/em> that represent the parameters\ndefined in either \u201c<code>producer server-<\/code><em>n<\/em>\u201d\nor \u201c<code>consumer server-<\/code><em>n<\/em>\u201d configuration\nproperties. The other arguments indicate whether this is a producer or a\nconsumer server and the <em>n<\/em> value of\nthe property key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The configuration of an SSL server requires the following parameters:<\/p>\n\n\n\n<ul class=\"nobullet\"><li><code>port<\/code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; the port number that the server listens on.<\/li><li><code>ks<\/code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a path to the file that contains the Server Key Store.<\/li><li><code>pwd<\/code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; the Key Store password.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The method verifies that the <code>port<\/code> parameter exists and specifies a valid number, then saves the\nkey store path and password as System properties.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The next is the abstract <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/ServerChannel.html#open()\">open()<\/a> method implemented in line 56. It creates\nan <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/javax\/net\/ssl\/SSLServerSocket.html\">SSLServerSocket<\/a> object that listens to the\nspecified port and saves it for later use.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The implementation of the abstract <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/ServerChannel.html#accept()\">accept()<\/a> method (line 67) calls <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/java\/net\/ServerSocket.html#accept%28%29\">ServerSocket.accept()<\/a> that listens for a\nconnection to be made to this socket and accepts it. The received <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/java\/net\/Socket.html\">Socket<\/a> object is wrapped by an <code>SslSocket<\/code> object as explained below.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The implementation of the abstract <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/ServerChannel.html#close()\">close()<\/a> method (line 83) closes the saved <code>SSLServerSocket<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The last method is an implementation of the abstract <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/ServerChannel.html#getGateID()\">getGateID()<\/a> method (line 92). It returns an\nidentification of this server that is used by the <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/logging.html\">logging system<\/a> and <a href=\"http:\/\/spideradmin.com\">SpiderAdmin<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The other part of the mission is to build the communication\nplugin that wraps <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/javax\/net\/ssl\/SSLSocket.html\">SSLSocket<\/a>. This is done by extending <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html\">Channel<\/a>, implementing a few abstract methods and\noverriding a few other methods if necessary. We do it with <code>SslSocket<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson15.plugins;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.InetAddress;\nimport java.net.Socket;\nimport java.net.UnknownHostException;\nimport java.util.Map;\nimport javax.net.ssl.SSLSocket;\nimport javax.net.ssl.SSLSocketFactory;\nimport org.spiderwiz.core.Channel;\nimport org.spiderwiz.core.Main;\nimport org.spiderwiz.zutils.ZUtilities;\n\n\/**\n * Implementation of an SSL TCP\/IP socket plugin.\n *\/\npublic class SslSocket extends Channel{\n    private static final String NO_TCP_PARAMS = &quot;No IP address or port number specified for %1$s-%2$d&quot;;\n    private static final String TYPES[] = {&quot;import&quot;, &quot;producer&quot;, &quot;consumer&quot;};\n\n    private Socket socket = null;\n    private String ip;\n    private int port;\n\n    \/**\n     * Called by the framework to configure the socket with the parameters defined in\n     * &quot;producer-n&quot;, &quot;consumer-n&quot; or &quot;import-n&quot; configuration properties. The relevant parameters for an SSL socket are:\n     *      ip      the address of the server to connect to\n     *      port    the port number to connect to\n     *      ks      a path to a file that contains the client key store.\n     * @param configParams      a map of key=value configuration parameters.\n     * @param type              type of channel - 0: import, 1:producer, 2:consumer.\n     * @param n                 the &lt;em&gt;n&lt;\/em&gt; value of &lt;em&gt;import-n&lt;\/em&gt;, &lt;em&gt;producer-n&lt;\/em&gt; or &lt;em&gt;consumer-n&lt;\/em&gt;.\n     * @return true if and only if configuration is successful.\n     *\/\n    @Override\n    protected boolean configure(Map&lt;String, String&gt; configParams, int type, int n) {\n        ip = configParams.get(&quot;ip&quot;);\n        port = ZUtilities.parseInt(configParams.get(&quot;port&quot;));\n        if (ip == null || port &lt;= 0) {\n            Main.getInstance().sendExceptionMail(null, String.format(NO_TCP_PARAMS, TYPES[type], n), null, true);\n            return false;\n        }\n        socket = null;\n        String truststoreLocation = configParams.get(&quot;ks&quot;);\n        if (truststoreLocation != null)\n            System.setProperty(&quot;javax.net.ssl.trustStore&quot;, truststoreLocation);\n        return true;\n    }\n\n    \/**\n     * Creates an SSL socket connection to the server and perform handshake.\n     * @return true if and only if the operation is successful.\n     * @throws UnknownHostException\n     * @throws IOException\n     *\/\n    @Override\n    protected boolean open() throws UnknownHostException, IOException {\n        \/\/ create a socket only if this is a client side socket. A server side socket already exists\n        if (port != 0) {\n            SSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault();\n            socket = f.createSocket(InetAddress.getByName(ip).getHostAddress(), port);\n            ((SSLSocket)socket).startHandshake();\n        }\n        return true;\n    }\n\n    \/**\n     * Disconnects by closing the socket.\n     * @throws IOException\n     *\/\n    @Override\n    protected void close() throws IOException {\n        if (socket != null)\n            socket.close();\n        socket = null;\n    }\n\n    \/**\n     * @return the input stream of the connected socket\n     * @throws IOException\n     *\/\n    @Override\n    protected InputStream getInputStream() throws IOException {\n        return socket == null ? null : socket.getInputStream();\n    }\n\n    \/**\n     * @return the output stream of the connected socket.\n     * @throws IOException\n     *\/\n    @Override\n    protected OutputStream getOutputStream() throws IOException {\n        return socket == null ? null : socket.getOutputStream();\n    }\n\n    \/**\n     * A client socket should try to reconnect if disconnected while a server side socket shall not.\n     * @return true if this is a server side socket.\n     *\/\n    @Override\n    protected boolean dontReconnect() {\n        return port == 0;\n    }\n\n    \/**\n     * @return a string that represents the socket protocol and the address it is connected to in the form:\n     * &quot;ssl:&lt;ip address&gt;:&lt;port number&gt;&quot;. If this is a server side socket then the &lt;port number&gt; part is omitted.\n     *\/\n    @Override\n    public String getRemoteAddress() {\n        return &quot;ssl:&quot; + ip + (port == 0 ? &quot;&quot; : &quot;:&quot; + port);\n    }\n\n    \/**\n     * Called by SslServerSocket when the server accepts a connection and creates a Socket object to handle it on the server\n     * side. Attaches the Socket object to the current object and constructs the peer IP address.\n     * @param socket the Socket object that shall be attached to this object.\n     *\/\n    public void setSocket(Socket socket) {\n        this.socket = socket;\n        ip = socket.getRemoteSocketAddress().toString().replace(&quot;\/&quot;, &quot;&quot;);\n        int n = ip.indexOf(&quot;:&quot;);\n        if (n &gt; 0)\n            ip = ip.substring(0, n);\n        port = 0;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Here we also need to implement a <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#configure(java.util.Map,int,int)\">configure()<\/a> method (line 29). The parameters for\na client connection, defined in <code>producer-<\/code><em>n<\/em>,\n<code>consumer-<\/code><em>n<\/em> or <code>import-<\/code><em>n<\/em> configuration properties are:<\/p>\n\n\n\n<ul class=\"nobullet wp-block-list\"><li><code>ip<\/code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; the IP address or server name to connect to.<\/li><li><code>port<\/code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; the port number to connect to.<\/li><li><code>ks<\/code>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a path to the file that contains the Client Key Store.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The method verifies that the <code>ip<\/code> and <code>port<\/code> parameters\nexit and saves the client key store as a System property.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The override of the <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#open()\">open()<\/a> method (line 60) creates an <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/javax\/net\/ssl\/SSLSocket.html\">SSLSocket<\/a> object to handle the connection with\nthe configured server and uses it to start an <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/javax\/net\/ssl\/SSLSocket.html#startHandshake%28%29\">SSL handshake<\/a> on the connection. This is done\nonly if <code>port<\/code> does not equal zero. It\ndoes when the current object manages the server side of the connection, in\nwhich case the socket is already opened as we will see below.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The override of the <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#close()\">close()<\/a> method (line 75) closes the managed\nsocket.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The implementations of the abstract <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#getInputStream()\">getInputStream()<\/a> (line 86) and <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#getOutputStream()\">getOutputStream()<\/a> (line 95) methods return an\ninput stream and an output stream respectively for the managed socket. Note\nthat you could implement these methods to return <code>null<\/code> and override <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#readLine()\">readLine()<\/a> and <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#writeLine(java.lang.String)\">writeLine()<\/a> instead, but the former are\npreferable because they support the built-in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/compression.html\">data compression<\/a> at the physical level.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We override <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#dontReconnect()\">dontReconnect()<\/a> (line 104) to return <code>true<\/code> when <code>port<\/code> equals zero, because this is an indication that the current\nobject manages a server-side socket that should not try to reconnect after\ndisconnection. The other case is a client-side socket that should try to\nautomatically reconnect to the specified port after reconnection. (Users can\nconfigure the reattempts time interval using the <code>reconnection seconds<\/code> <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/config.html#reconnection\">configuration property<\/a>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We implement the abstract <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Channel.html#getRemoteAddress()\">getRemoteAddress()<\/a> (line 113) method to return\nthe address of the peer server, prefixed by the \u201c<code>ssl:<\/code>\u201d protocol indication. If this object manages a client side\nsocket then the port number is appended to the address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The last method is <code>setSocket()<\/code>\nthat is used by <code>SslServerSocket<\/code> to\nattach a server-side <code>Socket<\/code> object to\nthe current <code>Channel<\/code> object as we saw\nabove. The method also uses <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/java\/net\/Socket.html#getRemoteSocketAddress%28%29\">Socket.getRemoteSocketAddress()<\/a> to set the IP\naddress of the client and sets <code>port<\/code>\nto zero so that this will be identified as a server-side socket.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Our plugins are done. In order to test them, we implement <em>SSL Hub<\/em> \u2013 a command line Spiderwiz hub\nthat connects to <em>My Hub<\/em> of the\nprevious lessons and is also configured as a server using the new <code>SslServerSocket<\/code> plugin (we could also\nuse <em>My Hub<\/em> as a hybrid server but\nthat would require us to rebuild it, while we prefer not to touch it). Here is <code>SslHubMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson15.sslhub;\n\nimport org.spiderwiz.tutorial.objectLib.ConsoleMain;\n\n\/**\n * Main class for SSL Hub.\n *\/\npublic class SslHubMain extends ConsoleMain{\n    private static final String ROOT_DIRECTORY = &quot;\/tests\/SslHub&quot;;\n    private static final String CONFIG_FILE_NAME = &quot;sslhub.conf&quot;;\n    private static final String APP_NAME = &quot;SSL Hub&quot;;\n    private static final String APP_VERSION = &quot;Z15.01&quot;;  \/\/ Version Z15.01: SSL hub for Lesson 15\n\n    public SslHubMain() {\n        super(ROOT_DIRECTORY, CONFIG_FILE_NAME, APP_NAME, APP_VERSION);\n    }\n\n    \/**\n     * Application entry point. Instantiate the class and initialize the instance.\n     * \n     * @param args the command line arguments. Not used in this application.\n     *\/\n    public static void main(String[] args) {\n        new SslHubMain().init();\n    }\n\n    \/**\n     * @return an empty list as we produce nothing\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{};\n    }\n\n    \/**\n     * @return an empty list as we produce nothing\n     *\/\n    @Override\n    protected String[] getConsumedObjects() {\n        return new String[]{};\n    }\n\n    \/**\n     * Process an input line - nothing to process in our case.\n     * @param line      The input line\n     * @return true\n     *\/\n    @Override\n    protected boolean processConsoleLine(String line) {\n        return true;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Just a barebones Spiderwiz console application. We define it\nas a hub in its configuration file:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[log folder]Logs\n[consumer server-1]class=org.spiderwiz.tutorial.lesson15.plugins.SslServerSocket;port=10002;ks=C:\/Keys\/ServerKeyStore.jks;pwd=Pass1word\n[producer-1]ip=localhost;port=10001\n[hub mode]yes<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">A plugin server is configured through the same property as a\nbuilt-in server (<code>consumer server-1<\/code> in\nthis case), except that the definition includes the <code>class<\/code> parameter that specifies the fully qualified class name of\nthe plugin implementation. The other parameters are implementation dependent as\nwe saw above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same is done with a client plugin. Here is <code>chat1.conf<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[application name]Chat 1\n[log folder]\/tests\/Chat1\/Logs\n[backup folder]\/tests\/Chat1\/Backup\n[history file]\/tests\/Chat1\/history.dat\n[producer-1]class=org.spiderwiz.tutorial.lesson15.plugins.SslSocket;ip=localhost;port=10002;ks=C:\/CA\/ClientKeyStore.jks<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">In order for this to work, you need to rebuild both <em>SSL Hub<\/em> and the chat application with the plugin classes in the CLASSPATH.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s all. In the <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-16\/\">next\nlesson<\/a> we will build a Mail plugin.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to extend Spiderwiz by writing and using a communication plugin. In this exercise we will implement an SSL TCP\/IP connection. <\/p>\n","protected":false},"author":1,"featured_media":0,"parent":145,"menu_order":15,"comment_status":"closed","ping_status":"closed","template":"tutorial-child.php","meta":{"footnotes":""},"class_list":["post-1006","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1006","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/comments?post=1006"}],"version-history":[{"count":17,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1006\/revisions"}],"predecessor-version":[{"id":1427,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1006\/revisions\/1427"}],"up":[{"embeddable":true,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/145"}],"wp:attachment":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/media?parent=1006"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}