com.chaosinmotion.securechat.server.messages.NotificationSocket.java Source code

Java tutorial

Introduction

Here is the source code for com.chaosinmotion.securechat.server.messages.NotificationSocket.java

Source

/*   SecureChat: A secure chat system which permits secure communications 
 *  between iOS devices and a back-end server.
 *
 *   Copyright  2016 by William Edward Woody
 *
 *   This program 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 3 of the License, or (at your 
 *   option) any later version.
 *
 *   This program 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, see <http://www.gnu.org/licenses/>
 */

package com.chaosinmotion.securechat.server.messages;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.UUID;
import org.json.JSONObject;
import org.json.JSONTokener;
import com.chaosinmotion.securechat.server.database.Database;
import com.chaosinmotion.securechat.server.utils.Hash;
import com.chaosinmotion.securechat.shared.Constants;

/**
 * Network socket listening for incoming requests form a particular device.
 * @author woody
 *
 */
public class NotificationSocket implements Runnable {
    private Socket socket;
    private NotificationService ns;
    private Thread thread;

    private SCOutputStream out;
    private SCInputStream in;

    private String token;
    private int deviceID;

    private static SimpleDateFormat format;
    static {
        format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    }

    public NotificationSocket(Socket s, NotificationService n) {
        socket = s;
        ns = n;
        thread = new Thread(this);
        thread.start();
    }

    /**
     * Force terminate of this socket.
     */
    public synchronized void terminate() {
        try {
            out.close();
            in.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * Validate username/password and start message pump
     * @param username
     * @param password
     * @return
     */
    private int validate(String username, String password, String deviceid) {
        Connection c = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        int dev = 0;

        try {
            c = Database.get();
            ps = c.prepareStatement("SELECT Users.password, Devices.deviceid " + "FROM Users, Devices "
                    + "WHERE Users.username = ? " + "  AND Devices.userid = Users.userid "
                    + "  AND Devices.deviceuuid = ?");
            ps.setString(1, username);
            ps.setString(2, deviceid);
            rs = ps.executeQuery();

            if (rs.next()) {
                /*
                 * If the result is found, hash the entry in the way it would
                 * be hashed by the front end, and compare to see if the
                 * hash codes match. (This requires that the hashed password
                 * stored in the back-end has a consistent capitalization.
                 * We arbitrarily pick lower-case for our SHA-256 hex string.
                 */
                String spassword = rs.getString(1);
                dev = rs.getInt(2);

                /*
                 * Encrypt password with token and salt
                 */

                spassword = spassword + Constants.SALT + token;
                spassword = Hash.sha256(spassword);

                /*
                 * Compare; if matches, then return the user info record
                 * so we can store away. While the SHA256 process returns
                 * consistent case, we compare ignoring case anyway, just
                 * because. :-)
                 */

                if (!spassword.equalsIgnoreCase(password)) {
                    /*
                     * This fails to run.
                     */

                    return 1;
                }
            } else {
                return 2;
            }

            /*
             * At this point we're logged in. Register for real time messages
             * and send all of the stored messages for this device
             */

            rs.close();
            rs = null;
            ps.close();
            ps = null;

            /*
             * Register for real-time messages. There is a small window in
             * which we may write messages out of order. We rely on the
             * client to sort the messages correctly.
             */

            deviceID = dev;
            MessageQueue.getInstance().registerNotification(dev, NotificationSocket.this);

            /*
             * Run query to get messages, and send them to the calling
             * device. 
             */

            ps = c.prepareStatement("SELECT Messages.messageid, " + "    Messages.senderid, "
                    + "    Users.username, " + "    Messages.toflag, " + "    Messages.received, "
                    + "    Messages.message " + "FROM Messages, Users " + "WHERE Messages.deviceid = ? "
                    + "  AND Messages.senderid = Users.userid");
            ps.setInt(1, deviceID);

            rs = ps.executeQuery();
            while (rs.next()) {
                int messageID = rs.getInt(1);
                int senderID = rs.getInt(2);
                String senderName = rs.getString(3);
                boolean toflag = rs.getBoolean(4);
                Timestamp received = rs.getTimestamp(5);
                byte[] message = rs.getBytes(6);

                sendMessage(messageID, senderID, senderName, toflag, received, message);
            }
        } catch (Exception ignore) {
            return 3;
        } finally {
            try {
                if (rs != null)
                    rs.close();
                if (ps != null)
                    ps.close();
                if (c != null)
                    c.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        return 0;
    }

    /**
     * Parse the JSON command. We handle the commands 'token' and 'login'.
     * @param obj
     */
    private void processJSONCommand(JSONObject obj) {
        // Unknown commands are ignored.

        String cmd = obj.getString("cmd");

        if (cmd.equalsIgnoreCase("token")) {
            /*
             * Write header and token (as string without length)
             */
            try {
                token = UUID.randomUUID().toString();
                byte[] b = token.getBytes("UTF-8");
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                baos.write(0x21);
                baos.write(b);
                out.writeData(baos.toByteArray());
            } catch (Exception ex) {
            }

        } else if (cmd.equalsIgnoreCase("login")) {
            /*
             * Validate login for this
             */

            String username = obj.getString("username");
            String password = obj.getString("password");
            String deviceid = obj.getString("deviceid");

            /*
             * Validate
             */

            int err = validate(username, password, deviceid);
            if (err != 0) {
                byte[] b = new byte[2];
                b[0] = 0x22;
                b[1] = (byte) err;
                try {
                    out.writeData(b);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Listen for incoming requests on the socket. This starts up an
     * input stream, which receives commands in JSON format with the
     * format { "cmd": command, ... params ... }
     */
    @Override
    public void run() {
        try {
            /*
             * Wrap the input/output streams and start processing requests.
             */
            out = new SCOutputStream(socket.getOutputStream());
            in = new SCInputStream(socket.getInputStream()) {
                @Override
                public void processPacket(byte[] data) {
                    try {
                        String json = new String(data, "UTF-8");
                        JSONTokener t = new JSONTokener(json);
                        JSONObject obj = new JSONObject(t);

                        /*
                         * Parse and process the command
                         */

                        processJSONCommand(obj);
                    } catch (UnsupportedEncodingException e) {
                        // Should never happen
                        e.printStackTrace();
                    }
                }
            };
            in.processStream();

            /*
             * When we reach here, we've been closed.
             */

            ns.removeNotificationSocket(NotificationSocket.this);
            if (deviceID != 0) {
                MessageQueue.getInstance().unregisterNotification(deviceID);
            }
        } catch (Throwable th) {
            // ignore.
        }
    }

    /**
     * Internal method for sending a message to the specified device. This
     * encodes the message as a binary array and transmits it as a single 
     * packet to the listening device. This allows users to receive messages
     * during chat as soon as we are able to, for (more or less) just in time
     * messaging.
     * 
     * The packet returned here is similar to the packet returned by the
     * getmessages api, except we serialize as binary.
     * 
     * @param messageid
     * @param senderid
     * @param sendername
     * @param ts
     * @param message
     * @throws IOException 
     */
    void sendMessage(int messageid, int senderid, String sendername, boolean toflag, Timestamp ts, byte[] message)
            throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);

        /*
         * Encode. First byte is 0x20
         */

        String date;
        synchronized (format) {
            date = format.format(ts);
        }

        /*
         * Formulate packet in expected format
         */
        dos.writeByte(0x20); // marker
        dos.writeBoolean(toflag);
        dos.writeInt(messageid);
        dos.writeInt(senderid);
        dos.writeUTF(date);
        dos.writeUTF(sendername);
        dos.writeInt(message.length);
        dos.write(message);

        /*
         * Flush and write packet to device. Our protocol does not depend on
         * the device actually receiving this message, as we wait until the
         * device deletes the messages by a separate command.
         */
        dos.flush();
        out.writeData(baos.toByteArray());
    }

}