Java Flash communications over XMLSocket

I’ve recently been working on a Java server which will communicate with a Flash frontend. When I started I wanted to use embedded Flash socket code, because I’m more comfortable with Java. Hence I decided to use the XMLSocket protocol. Along the way I’ve found many limitations for this protocol (such as the lack of SSL support), never-the-less I implemented a server side.

The format is and XML file followed by a 0 byte. Unfortunately, there seem to be no pre-written Java classes to read such files. Therefore I wrote one, and decided it’s had enough testing to publish to the world. It’s not amazing, but I physically can’t see any other way to optimise it without losing functionality somewhere. It uses JDOM (it’s all I know), although could probably be easily ported to another Java XML framework.

Anyway here is the code, enjoy it… Improvements welcome and encouraged!

package sockets;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import org.jdom.Document;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;

/**
 *
 * @author chris
 */
public class XMLSocket {

    /** Maximum read length so we can't be attacked by memory usage - 10KB */
    private final static int MAX_READ_LENGTH = 10240;

    /** The socket which will be written to/read from */
    private final Socket socket;
    /** The buffer which is read into */
    private byte buffer[];
    /** The XML read so far */
    private String xml;
    /** Whether to discard until the next NULL byte */
    private boolean discard;
    /** The ammount if bytes left in the buffer */
    private int bufferRead;
    /** The InputSteam of the socket to write to */
    private InputStream in;
    /** The OutputStream of the socket to read from */
    private OutputStream out;

    /**
     * Initialise a reader and writer for XML with 0 characters between
     * XML documents. This is a Java implementation of the Flash XMLSocket.
     * @param s The socket which should be read from/written to
     * @throws java.io.IOException If there is a problem opening the input or output streams
     */
    public XMLSocket(Socket s) throws IOException {
        socket = s;
        xml = null;
        discard = false;
        in = s.getInputStream();
        out = s.getOutputStream();
        buffer = new byte[1024];
        bufferRead = 0;
    }

    /**
     * Returns whether the socket is closed
     * @return true is the socket is closed
     */
    public boolean isClosed() {
        return socket.isClosed();
    }

    /**
     * Search a byte array for the first index of a specfic element
     * @param array The array to search for element
     * @param element The element to search for
     * @param length The maximum number of elements to search (1 indexed)
     * @return The index of the element or -1 if does not exist
     */
    private int indexOf(byte array[], byte element, int length) {
        if (length > array.length)
        length = array.length;
        for (int i = 0; i < length; i++) {
            if (array[i] == element) return i;
        }
        return -1;
    }
    
    /**
     * Reads from the socket the next XML data. Each packet of data should
     * be seperated by a 0 character. Will return null if the data between 0
     * characters is longer than MAX_READ_LENGTH, but the socket will continue
     * to search for the next 0. This should not cause a memory issue, but
     * could result in a DoS attack.
     *
     * @param blocking Time to block for waiting for a document (0 is infinity, < 100 will cause the system not to loop)
     * @param readLength Length (in kb packets) to try to read before closing the connection (0 is inifinity)
     * @return Null if there was a problem reading or the next read was too large
     * @throws java.io.IOException If there is a problem reading from the socket
     * @throws org.jdom.JDOMException If there is a problem with the XML read
     */
    public synchronized Document readXML(int blocking, int readLength) throws IOException, JDOMException {
        if (in == null) return null;
        try {
            socket.setSoTimeout(blocking);
        } catch (SocketException e) {
            // Cannot set blocking time... should not risk blocking
            return null;
        }
        // Number of reads from the socket
        int reads = 0;
        // Do a pre-read to test socket
        if (bufferRead == 0) {
            try {
                int r = in.read();
                if (r == -1) {
                    // Socket closed
                    throw new IOException();
                }
                buffer[0] = (byte)r;
                bufferRead = 1;
            } catch (SocketTimeoutException e) {
                return null;
            }
        }
        int zeroIndex = -1;
        while (zeroIndex == -1) {
            zeroIndex = indexOf(buffer, (byte)0, bufferRead);
            if (zeroIndex == -1) {
                // EOF not found, read more
                if (xml != null && xml.length() >= MAX_READ_LENGTH) {
                    // Max read length, ignore input
                    xml = null;
                    discard = true;
                }

                if (discard == true) {
                    // We're discarding due to data size, do some throttling
                    // to limit effect of DoS attacks
                    try {
                        // Is this the optimal sleep for 1k of data each loop?
                        Thread.sleep(50);
                    } catch (Exception e) {
                        // Only throttling, not bothered about exceptions
                    }
                } else {
                    if (xml == null) {
                        xml = new String(buffer, 0, bufferRead);
                    } else {
                        xml += new String(buffer, 0, bufferRead);
                    }
                    bufferRead = 0;
                }

                // After the first read subsequent ones should complete faster
                if (reads == 1) {
                    if (blocking > 0 && blocking < 100) {
                        return null;
                    } else {
                        try {
                            socket.setSoTimeout(100);
                        } catch (SocketException e) {
                            // Cannot set blocking time... should not risk blocking
                            return null;
                        }
                    }
                }
                try {
                    bufferRead = in.read(buffer, 0, buffer.length);
                } catch (SocketTimeoutException e) {
                    bufferRead = -1;
                }
                if (bufferRead == -1) {
                    bufferRead = 0;
                    return null;
                }
                reads++;
                if (readLength-- == 0) {
                    // The maximum read length has been reached, close the socket
                    // and return null
                    close();
                    return null;
                }
                if (reads == 32) {
                    // Taken too many reads of junk
                    close();
                    return null;
                }
            } else {
                if (discard == false && zeroIndex > 0) {
                    xml += new String(buffer, 0, zeroIndex - 1);
                }
                bufferRead -= zeroIndex + 1;
                if (bufferRead <= 0) {
                    bufferRead = 0;
                } else {
                    System.arraycopy(buffer, zeroIndex + 1, buffer, 0, bufferRead);
                }

                if (discard == false) {
                    xml = xml.trim();
                }
            }
        }

        // If we got this far the read data is in xml, or discard is true
        if (discard == true || xml == null) {
            // The last read was too large. This could still be ok.
            return null;
        } else {
            // Set xml to NULL first incase there is an exception thrown
            String temp = xml;
            xml = null;
            return new SAXBuilder().build(new StringReader(temp));
        }
    }

    /**
     * Write XML document to socket followed by a 0 character.
     * @param d XML Document to write to the socket
     * @return The document converted to bytes to send, this can be used to
     * speed multiple sends, null if the connection failed
     */
    public byte[] writeXML(Document d) {
        if (out == null) return null;
        byte[] x = checkNullTermination(new XMLOutputter().outputString(d).getBytes());
        if (writeXML(x)) {
            return x;
        } else {
            return null;
        }
    }

    /**
     * Checks that the byte array terminates with a null, if not it adds one
     * @param xml The byte array to check
     * @return A definately NULL terminated byte array based on the input
     */
    private byte[] checkNullTermination(byte xml[]) {
        byte send[];
        // If there is a usless character at the end, we can use it
        // for the 0 byte rather than wasting memory/CPU time.
        if (xml[xml.length - 1] == '\n' || xml[xml.length - 1] == 0) {
            xml[xml.length - 1] = 0;
            send = xml;
        } else {
            send = new byte[xml.length + 1];
            System.arraycopy(xml, 0, send, 0, xml.length);
            send[xml.length] = 0;
        }
        return send;
    }

    /**
     * Write XML document to socket followed by a 0 character.
     * @param xml The XML represented in a sendable form
     * @return true if written successfully, false otherwise
     */
    public boolean writeXML(byte[] xml) {
        if (out == null) return false;
        try {
            xml = checkNullTermination(xml);
            synchronized(socket) {
                out.write(xml);
                out.flush();
            }
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Close input part of the socket
     */
    public void closeInput() {
        try {
            in.close();
        } catch (Exception e) {}
        in = null;
        buffer = null;
    }

    /**
     * Close output part of the socket
     */
    public void closeOutput() {
        try {
            out.close();
        } catch (Exception e) {}
        out = null;
    }

    /**
     * Close the socket and all I/O
     */
    public void close() {
        closeInput();
        closeOutput();
        try {
            socket.close();
        } catch (Exception e) {}
    }

    /**
     * Returns the InetAddress of the remote connected server
     * @return The InetAddress of the remote connected server
     */
    public InetAddress getInetAddress() {
        return socket.getInetAddress();
    }

}

Leave a Reply

Your email address will not be published. Required fields are marked *