Add 'src/be/nikiroo/utils/' from commit '46add0670fdee4bd936a13fe2448c5e20a7ffd0a'
[fanfix.git] / src / be / nikiroo / utils / serial / server / ConnectAction.java
index dfdb53ed017da95df65d80f5b8111fc5e0fdc153..6a19368bbf97824deb624ffaf9eac238b44dfad7 100644 (file)
@@ -1,14 +1,23 @@
 package be.nikiroo.utils.serial.server;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.Socket;
 
+import javax.net.ssl.SSLException;
+
+import be.nikiroo.utils.CryptUtils;
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.StringUtils;
 import be.nikiroo.utils.Version;
 import be.nikiroo.utils.serial.Exporter;
 import be.nikiroo.utils.serial.Importer;
+import be.nikiroo.utils.streams.BufferedOutputStream;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
+import be.nikiroo.utils.streams.ReplaceInputStream;
+import be.nikiroo.utils.streams.ReplaceOutputStream;
 
 /**
  * Base class used for the client/server basic handling.
@@ -20,14 +29,23 @@ import be.nikiroo.utils.serial.Importer;
  * @author niki
  */
 abstract class ConnectAction {
+       // We separate each "packet" we send with this character and make sure it
+       // does not occurs in the message itself.
+       static private char STREAM_SEP = '\b';
+       static private String[] STREAM_RAW = new String[] { "\\", "\b" };
+       static private String[] STREAM_CODED = new String[] { "\\\\", "\\b" };
+
        private Socket s;
        private boolean server;
-       private Version version;
+
        private Version clientVersion;
+       private Version serverVersion;
+
+       private CryptUtils crypt;
 
        private Object lock = new Object();
-       private BufferedReader in;
-       private OutputStreamWriter out;
+       private NextableInputStream in;
+       private BufferedOutputStream out;
        private boolean contentToSend;
 
        /**
@@ -35,7 +53,8 @@ abstract class ConnectAction {
         * client or server this {@link ConnectAction} represent.
         * 
         * @param version
-        *            the counter part version
+        *            the version on the other side of the communication (client or
+        *            server)
         * 
         * @throws Exception
         *             in case of I/O error
@@ -60,7 +79,8 @@ abstract class ConnectAction {
         * Handler called when an unexpected error occurs in the code.
         * 
         * @param e
-        *            the exception that occurred
+        *            the exception that occurred, SSLException usually denotes a
+        *            crypt error
         */
        abstract protected void onError(Exception e);
 
@@ -72,20 +92,30 @@ abstract class ConnectAction {
         * @param server
         *            TRUE for a server action, FALSE for a client action (will
         *            impact the process)
+        * @param key
+        *            an optional key to encrypt all the communications (if NULL,
+        *            everything will be sent in clear text)
         * @param version
-        *            the version of this client-or-server
+        *            the client-or-server version (depending upon the boolean
+        *            parameter <tt>server</tt>)
         */
-       protected ConnectAction(Socket s, boolean server, Version version) {
+       protected ConnectAction(Socket s, boolean server, String key,
+                       Version version) {
                this.s = s;
                this.server = server;
+               if (key != null) {
+                       crypt = new CryptUtils(key);
+               }
 
                if (version == null) {
-                       this.version = new Version();
-               } else {
-                       this.version = version;
+                       version = new Version();
                }
 
-               clientVersion = new Version();
+               if (server) {
+                       serverVersion = version;
+               } else {
+                       clientVersion = version;
+               }
        }
 
        /**
@@ -94,7 +124,29 @@ abstract class ConnectAction {
         * @return the version
         */
        public Version getVersion() {
-               return version;
+               if (server) {
+                       return serverVersion;
+               }
+
+               return clientVersion;
+       }
+
+       /**
+        * The total amount of bytes received.
+        * 
+        * @return the amount of bytes received
+        */
+       public long getBytesReceived() {
+               return in.getBytesRead();
+       }
+
+       /**
+        * The total amount of bytes sent.
+        * 
+        * @return the amount of bytes sent
+        */
+       public long getBytesWritten() {
+               return out.getBytesWritten();
        }
 
        /**
@@ -102,35 +154,34 @@ abstract class ConnectAction {
         */
        public void connect() {
                try {
-                       in = new BufferedReader(new InputStreamReader(s.getInputStream(),
-                                       "UTF-8"));
+                       in = new NextableInputStream(s.getInputStream(),
+                                       new NextableInputStreamStep(STREAM_SEP));
                        try {
-                               out = new OutputStreamWriter(s.getOutputStream(), "UTF-8");
+                               out = new BufferedOutputStream(s.getOutputStream());
                                try {
+                                       // Negotiate version
+                                       Version version;
                                        if (server) {
-                                               String line = in.readLine();
-                                               if (line != null && line.startsWith("VERSION ")) {
-                                                       // "VERSION client-version" (VERSION 1.0.0)
-                                                       Version clientVersion = new Version(
-                                                                       line.substring("VERSION ".length()));
-                                                       this.clientVersion = clientVersion;
-                                                       Version v = negotiateVersion(clientVersion);
-                                                       if (v == null) {
-                                                               v = new Version();
-                                                       }
-
-                                                       sendString("VERSION " + v.toString());
+                                               String HELLO = recString();
+                                               if (HELLO == null || !HELLO.startsWith("VERSION ")) {
+                                                       throw new SSLException(
+                                                                       "Client used bad encryption key");
                                                }
-
-                                               action(clientVersion);
+                                               version = negotiateVersion(new Version(
+                                                               HELLO.substring("VERSION ".length())));
+                                               sendString("VERSION " + version);
                                        } else {
-                                               String v = sendString("VERSION " + version.toString());
-                                               if (v != null && v.startsWith("VERSION ")) {
-                                                       v = v.substring("VERSION ".length());
+                                               String HELLO = sendString("VERSION " + clientVersion);
+                                               if (HELLO == null || !HELLO.startsWith("VERSION ")) {
+                                                       throw new SSLException(
+                                                                       "Server did not accept the encryption key");
                                                }
-
-                                               action(new Version(v));
+                                               version = new Version(HELLO.substring("VERSION "
+                                                               .length()));
                                        }
+
+                                       // Actual code
+                                       action(version);
                                } finally {
                                        out.close();
                                }
@@ -156,8 +207,9 @@ abstract class ConnectAction {
         * @param data
         *            the data to send
         * 
-        * @return the answer (which can be NULL) if this action is a client, always
-        *         NULL if it is a server
+        * @return the answer (which can be NULL if no answer, or NULL for an answer
+        *         which is NULL) if this action is a client, always NULL if it is a
+        *         server
         * 
         * @throws IOException
         *             in case of I/O error
@@ -172,14 +224,7 @@ abstract class ConnectAction {
         */
        protected Object sendObject(Object data) throws IOException,
                        NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
-               synchronized (lock) {
-                       String rep = sendString(new Exporter().append(data).toString(true));
-                       if (rep != null) {
-                               return new Importer().read(rep).getValue();
-                       }
-
-                       return null;
-               }
+               return send(out, data, false);
        }
 
        /**
@@ -209,12 +254,7 @@ abstract class ConnectAction {
        protected Object recObject() throws IOException, NoSuchFieldException,
                        NoSuchMethodException, ClassNotFoundException,
                        java.lang.NullPointerException {
-               String str = recString();
-               if (str == null) {
-                       throw new NullPointerException("No more data available");
-               }
-
-               return new Importer().read(str).getValue();
+               return rec(false);
        }
 
        /**
@@ -229,11 +269,117 @@ abstract class ConnectAction {
         * 
         * @throws IOException
         *             in case of I/O error
+        * @throws SSLException
+        *             in case of crypt error
         */
        protected String sendString(String line) throws IOException {
+               try {
+                       return (String) send(out, line, true);
+               } catch (NoSuchFieldException e) {
+                       // Cannot happen
+                       e.printStackTrace();
+               } catch (NoSuchMethodException e) {
+                       // Cannot happen
+                       e.printStackTrace();
+               } catch (ClassNotFoundException e) {
+                       // Cannot happen
+                       e.printStackTrace();
+               }
+
+               return null;
+       }
+
+       /**
+        * Reserved for the server (externally): flush the data to the client and
+        * retrieve its answer.
+        * <p>
+        * Also used internally for the client (only do something if there is
+        * contentToSend).
+        * <p>
+        * Will only flush the data if there is contentToSend.
+        * 
+        * @return the answer (which can be NULL if no more content)
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        * @throws SSLException
+        *             in case of crypt error
+        */
+       protected String recString() throws IOException {
+               try {
+                       return (String) rec(true);
+               } catch (NoSuchFieldException e) {
+                       // Cannot happen
+                       e.printStackTrace();
+               } catch (NoSuchMethodException e) {
+                       // Cannot happen
+                       e.printStackTrace();
+               } catch (ClassNotFoundException e) {
+                       // Cannot happen
+                       e.printStackTrace();
+               } catch (NullPointerException e) {
+                       // Should happen
+                       e.printStackTrace();
+               }
+
+               return null;
+       }
+
+       /**
+        * Serialise and send the given object to the counter part (and, only for
+        * client, return the deserialised answer -- the server will always receive
+        * NULL).
+        * 
+        * @param out
+        *            the stream to write to
+        * @param data
+        *            the data to write
+        * @param asString
+        *            TRUE to write it as a String, FALSE to write it as an Object
+        * 
+        * @return the answer (which can be NULL if no answer, or NULL for an answer
+        *         which is NULL) if this action is a client, always NULL if it is a
+        *         server
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        * @throws SSLException
+        *             in case of crypt error
+        * @throws IOException
+        *             in case of I/O error
+        * @throws NoSuchFieldException
+        *             if the serialised data contains information about a field
+        *             which does actually not exist in the class we know of
+        * @throws NoSuchMethodException
+        *             if a class described in the serialised data cannot be created
+        *             because it is not compatible with this code
+        * @throws ClassNotFoundException
+        *             if a class described in the serialised data cannot be found
+        */
+       private Object send(BufferedOutputStream out, Object data, boolean asString)
+                       throws IOException, NoSuchFieldException, NoSuchMethodException,
+                       ClassNotFoundException, java.lang.NullPointerException {
+
                synchronized (lock) {
-                       out.write(line);
-                       out.write("\n");
+                       OutputStream sub;
+                       if (crypt != null) {
+                               sub = crypt.encrypt64(out.open());
+                       } else {
+                               sub = out.open();
+                       }
+
+                       sub = new ReplaceOutputStream(sub, STREAM_RAW, STREAM_CODED);
+                       try {
+                               if (asString) {
+                                       sub.write(StringUtils.getBytes(data.toString()));
+                               } else {
+                                       new Exporter(sub).append(data);
+                               }
+                       } finally {
+                               sub.close();
+                       }
+
+                       out.write(STREAM_SEP);
 
                        if (server) {
                                out.flush();
@@ -241,25 +387,57 @@ abstract class ConnectAction {
                        }
 
                        contentToSend = true;
-                       return recString();
+                       try {
+                               return rec(asString);
+                       } catch (NullPointerException e) {
+                               // We accept no data here for Objects
+                       }
+
+                       return null;
                }
        }
 
        /**
-        * Reserved for the server (externally): flush the data to the client and
-        * retrieve its answer.
+        * Reserved for the server: flush the data to the client and retrieve its
+        * answer.
         * <p>
         * Also used internally for the client (only do something if there is
         * contentToSend).
         * <p>
         * Will only flush the data if there is contentToSend.
+        * <p>
+        * Note that the behaviour is slightly different for String and Object
+        * reading regarding exceptions:
+        * <ul>
+        * <li>NULL means that the counter part has no more data to send</li>
+        * <li>All the exceptions except {@link IOException} are there for Object
+        * conversion</li>
+        * </ul>
         * 
-        * @return the answer (which can be NULL)
+        * @param asString
+        *            TRUE for String reading, FALSE for Object reading (which can
+        *            still be a String)
+        * 
+        * @return the deserialised answer (which can actually be NULL)
         * 
         * @throws IOException
         *             in case of I/O error
+        * @throws NoSuchFieldException
+        *             if the serialised data contains information about a field
+        *             which does actually not exist in the class we know of
+        * @throws NoSuchMethodException
+        *             if a class described in the serialised data cannot be created
+        *             because it is not compatible with this code
+        * @throws ClassNotFoundException
+        *             if a class described in the serialised data cannot be found
+        * @throws java.lang.NullPointerException
+        *             for Objects only: if the counter part has no data to send
         */
-       protected String recString() throws IOException {
+       @SuppressWarnings("resource")
+       private Object rec(boolean asString) throws IOException,
+                       NoSuchFieldException, NoSuchMethodException,
+                       ClassNotFoundException, java.lang.NullPointerException {
+
                synchronized (lock) {
                        if (server || contentToSend) {
                                if (contentToSend) {
@@ -267,7 +445,27 @@ abstract class ConnectAction {
                                        contentToSend = false;
                                }
 
-                               return in.readLine();
+                               if (in.next() && !in.eof()) {
+                                       InputStream read = new ReplaceInputStream(in.open(),
+                                                       STREAM_CODED, STREAM_RAW);
+                                       try {
+                                               if (crypt != null) {
+                                                       read = crypt.decrypt64(read);
+                                               }
+
+                                               if (asString) {
+                                                       return IOUtils.readSmallStream(read);
+                                               }
+
+                                               return new Importer().read(read).getValue();
+                                       } finally {
+                                               read.close();
+                                       }
+                               }
+
+                               if (!asString) {
+                                       throw new NullPointerException();
+                               }
                        }
 
                        return null;