/** * Copyright 2009 Marc Stogaitis and Mimi Sun * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gmote.common; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.StreamCorruptedException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.security.NoSuchAlgorithmException; import java.util.logging.Level; import java.util.logging.Logger; import org.gmote.common.Protocol.Command; import org.gmote.common.Protocol.ServerErrorType; import org.gmote.common.packet.AbstractPacket; import org.gmote.common.packet.AuthenticationReq; import org.gmote.common.packet.ServerErrorPacket; import org.gmote.common.packet.SimplePacket; import org.gmote.common.security.AuthenticationException; import org.gmote.common.security.AuthenticationHandler; import org.gmote.common.security.IncompatibleClientException; /** * Container to hold a connection to a remote host. It has two constructors to * allow us either to wait for an incoming connection or to connect to a remote * host. Allows us to send data to the remote host. It also starts a thread that * listens for incoming data and notifies the appropriate DataReceiverIF. * * @author Marc * */ public class TcpConnection { private static final int CONNECTION_ESTABLISH_TIMEOUT = 1000 * 3; final private static Logger LOGGER = Logger.getLogger(TcpConnection.class .getName()); private static ServerSocket mServer = null; private Socket connectionSocket; private ObjectOutputStream connectionOutput; private ObjectInputStream connectionInput; private DataReceiverIF receiver; // Who to notify when data is received. private AuthenticationHandler authHandler; private String sessionId = null; /** * Constructor typically used when setting up a server. Should call the * listenForConnections method after calling this constructor. * */ public TcpConnection(AuthenticationHandler authHandler) { this.authHandler = authHandler; } /** * Listens for connections from incoming clients. * * @param port * the port to listen on. * @param receiver * an object that implements a method which can be called * asynchronously when data is received from the client. * @param passProvider * an object that supplies the password that must be entered by a * client in order to establish a connection. * * @return A pointer to the new data handling thread, or null if * startNewThread is false. * @throws IOException * @throws StreamCorruptedException * if the stream is not a java object stream. For example, this * exception is thrown when an HTTP client tries to establish a * connection. It can be safely caught and safely re-opened into a * non-object stream for furthur processing. */ public Thread listenForConnections(int port, DataReceiverIF receiver, PasswordProvider passProvider) throws IOException, StreamCorruptedException { // Wait for a connection. System.out.println("Waiting for a connection on port: " + port); ServerSocket server = getServerInstance(port); Socket serverSocket = server.accept(); String password = passProvider.fetchPassword(); handleConnectionEstablished(serverSocket, receiver, false); boolean authSucceeded = handleServerSideAuthentication(serverSocket, password); if (!authSucceeded) { return null; } return startPacketReceivingThread(); } private static ServerSocket getServerInstance(int port) throws IOException { if (mServer == null) { mServer = new ServerSocket(port); } return mServer; } /** * Allows the program to connect to a remote host. A separate listening thread * will be launched to handle packet reception * * @param port * @param host * @param receiver * Will be notified when data arrives. * @throws IOException * @throws ServerOutOfDateException * If the server is out of date. Note that if this exception is * thrown, the listening thread won't be established but we'll still * establish a connection to the server in order to allow the client * to notify the server that it should update itself. * @returns A pointer to the thread that is launched. */ public Thread connectToServerAsync(int port, String host, DataReceiverIF receiver, int timeout, String password) throws IOException, AuthenticationException, ServerOutOfDateException { // Establish a connection to the remote host. System.out.println("Connecting to host: " + host + ":" + port); InetAddress address = InetAddress.getByName(host); InetSocketAddress socketAddress = new InetSocketAddress(address, port); Socket remoteHost = new Socket(); remoteHost.connect(socketAddress, timeout); System.out.println("Successfully connected to host: " + host + ":" + port); handleConnectionEstablished(remoteHost, receiver, true); handleClientSideAuthentication(password); return startPacketReceivingThread(); } /** * Allows the program to connect to a remote host. This is synchronous, * therefore the caller must call readPacket() on this object in order to * receive data. * * @throws ServerOutOfDateException * If the server is out of date. Note that if this exception is * thrown, we'll still establish a connection to the server in order * to allow the client to notify the server that it should update * itself. */ public void connectToServerSync(int port, String host, String password) throws IOException, AuthenticationException, ServerOutOfDateException { // Establish a connection to the remote host. System.out.println("Connecting to host: " + host + ":" + port); InetAddress address = InetAddress.getByName(host); Socket remoteHost = new Socket(address, port); System.out.println("Successfully connected to host: " + host + ":" + port); handleConnectionEstablished(remoteHost, receiver, true); handleClientSideAuthentication(password); } /** * Opens up the proper data streams for the connection. * * @throws IOException * if there was a problem opening a stream. * @throws StreamCorruptedException * if the stream is not a java object stream. For example, this * exception is thrown when an HTTP client tries to establish a * connection. It can be safely caught and safely re-opened into a * non-object stream for furthur processing. */ private void handleConnectionEstablished(Socket connectionSocket, DataReceiverIF receiver, boolean outputStreamFirst) throws IOException, StreamCorruptedException { this.connectionSocket = connectionSocket; this.receiver = receiver; if (outputStreamFirst) { // The client should construct the output stream first, since if both the // server and the client construct an input stream without having created // an output stream, both will lock and wait for an initial amount of data // to be sent. We need to construct the server's input stream first since, // in the case of an http request, we don't want those initial bytes // written (which get automatically written when you create // ObjectOutputStream). connectionOutput = new ObjectOutputStream(connectionSocket .getOutputStream()); } int originalSoTimeout = connectionSocket.getSoTimeout(); try { connectionSocket.setSoTimeout(CONNECTION_ESTABLISH_TIMEOUT); connectionInput = new ObjectInputStream(connectionSocket.getInputStream()); } finally { connectionSocket.setSoTimeout(originalSoTimeout); } if (!outputStreamFirst) { connectionOutput = new ObjectOutputStream(connectionSocket .getOutputStream()); } } private Thread startPacketReceivingThread() { // Start the receiving thread which will wait for data to arrive. DataReceiver receiverThread = new DataReceiver(this); receiverThread.start(); return receiverThread; } /** * Handles client side authentication. * * @param password * @throws AuthenticationException * @throws ServerOutOfDateException */ private void handleClientSideAuthentication(String password) throws IOException, AuthenticationException, ServerOutOfDateException { try { AuthenticationReq challenge = (AuthenticationReq) readPacket(); sendPacket(authHandler.generateReplyToChallenge(password, challenge .getChallenge())); AbstractPacket result = readPacket(); if (result.getCommand() != Command.SUCCESS) { throw new AuthenticationException(); } if (!authHandler.isVersionCompatible(challenge.getServerVersion())) { throw new ServerOutOfDateException("The Gmote server is out of date. Server Version: " + challenge.getServerVersion() + " Client Version: " + authHandler.getAppVersion(), challenge.getServerVersion()); } sessionId = challenge.getChallenge(); }catch (NoSuchAlgorithmException e) { e.printStackTrace(); }catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * Performs authentication on the server side. Returns true if the * authentication succeeded. */ private boolean handleServerSideAuthentication(Socket connectionSocket, String password) throws IOException { // Perform authentication. try { String challenge = authHandler.generateServerChallenge(); authHandler.performAuthentication(this, password, challenge); sendPacket(new SimplePacket(Command.SUCCESS)); sessionId = challenge; return true; } catch (AuthenticationException e) { LOGGER.log(Level.WARNING, "Authentication of client failed: " + connectionSocket.getRemoteSocketAddress()); sendPacket(new ServerErrorPacket(ServerErrorType.AUTHENTICATION_FAILURE.ordinal(), "Authentication Failed. You may have entered the wrong password.")); sleep(1000); connectionSocket.close(); connectionOutput.close(); connectionInput.close(); return false; } catch (IncompatibleClientException e) { LOGGER.log(Level.WARNING, "Authentication of client failed: " + e.getMessage()); // Sending a 'success' packet followed by an error packet. This is due // to backwards compatibility since <= 1.2 clients don't expect to // receive update notifications. sendPacket(new SimplePacket(Command.SUCCESS)); // Send this error packet for older clients that don't handle the update request. sendPacket(new ServerErrorPacket(ServerErrorType.INCOMPATIBLE_CLIENT.ordinal(), e.getMessage())); connectionSocket.close(); connectionOutput.close(); connectionInput.close(); return false; } } /** * Sends a packet over the network. */ public void sendPacket(AbstractPacket packet) throws IOException { connectionOutput.writeObject(packet); connectionOutput.flush(); connectionOutput.reset(); } /** * Allows the caller to read a packet. This should only be used in conjunction * with connectToServerSync(). */ public AbstractPacket readPacket() throws IOException, ClassNotFoundException { return (AbstractPacket) connectionInput.readObject(); } /** * Allows the caller to read a packet. This should only be used in conjunction * with connectToServerSync(). * @param timeout milliseconds to wait for read operation. */ public AbstractPacket readPacket(int timeout) throws IOException, ClassNotFoundException, SocketTimeoutException { try { connectionSocket.setSoTimeout(timeout); return (AbstractPacket) connectionInput.readObject(); } finally { connectionSocket.setSoTimeout(0); } } /** * Thread used to monitor the connection and handle incomming messages. * * @author Marc * */ public class DataReceiver extends Thread { // The TcpConnection that started this thread. TcpConnection connection; /** * * @param connection * The connection that this object is listening on. */ public DataReceiver(TcpConnection connection) { super("DataReceiver"); this.connection = connection; } @Override public void run() { // Receives data sent on the TCP connection. try { while (true) { AbstractPacket packet = (AbstractPacket) connectionInput.readObject(); receiver.handleReceiveData(packet, connection); } } catch (IOException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } catch (ClassNotFoundException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } finally { closeConnection(); } } } /** * Attempts to release the port. * */ public void closeConnection() { try { if (connectionSocket != null) { connectionSocket.close(); } } catch (IOException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } public InetAddress getConnectedClientAddress() { return connectionSocket.getInetAddress(); } public boolean isConnected() { return (connectionSocket == null) ? false : connectionSocket.isConnected() && !connectionSocket.isClosed(); } public Socket getConnectionSocket() { return connectionSocket; } public String getSessionId() { return sessionId; } private void sleep(long timeInMili) { try { Thread.sleep(timeInMili); } catch (InterruptedException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } }