/**
* 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.server;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;
import org.gmote.common.MulticastClient;
import org.gmote.common.Protocol.UdpPacketTypes;
import org.gmote.server.settings.PreferredPorts;
/**
* Listens on a Multicast port and returns the name and ip of this host.
* @author Marc
*
*/
public class MulticastServerThread implements Runnable {
final private static Logger LOGGER = Logger.getLogger(MulticastServerThread.class.getName());
public static final int MULTICAST_LISTENING_PORT = 9901;
public static final String GROUP_NAME = "230.0.0.1";
// The port used to exchange udp data. This is where we exchange data such as
// mouse packets. It can be the same as the udp service discovery port, or
// different.
private static int serverUdpListeningPort = MULTICAST_LISTENING_PORT;
private String groupName;
// The listening port of the current socket.
private int socketListeningPort;
private static InetAddress connectedClientIp = null;
/**
* Creates a thread that will send out discovery notifications. We explicitly
* listen on each local interface since this resolves the following bug which
* is related to having multiple network cards:
* <p>
* 1. Listen on socket without providing a specific ip (by default, java will
* listen on all network interfaces)
* </p>
* <p>
* 2. Receive an IP request from a client
* </p>
* <p>
* 3. If we have multiple local ip's (ex: a wifi connection and wired
* connection), we won't know which ip to return (and will often return the
* wrong one if we simply call InetAddress.getLocalHost)
*
* Note: If we pass in null for 'localIpAddress', the we will revert back to
* listening on all local ip addresses and take a guess as to which ip we
* should return to the client. This mechanism should only be used if there is
* a problem listening on local interfaces.
* </p>
*
*/
public MulticastServerThread(String groupName, int socketListeningPort) {
this.groupName = groupName;
this.socketListeningPort = socketListeningPort;
}
public void run() {
MulticastSocket socket = join(groupName, socketListeningPort);
if (socket == null) {
String message = "Unable to join the multicast socket. Is the computer connected to a network? Exiting Gmote.";
LOGGER.severe(message);
JOptionPane.showMessageDialog(null, message);
System.exit(1);
}
byte[] inBuffer = new byte[500];
LOGGER.info("Listening for udp packets on " + socketListeningPort);
int errorCount = 0;
while (true) {
try {
receivePacket(socket, inBuffer);
} catch (Exception e) {
// Catching all exceptions here since this is the top level of our
// multicast thread and we never want it to die.
LOGGER.log(Level.SEVERE, e.getMessage(), e);
errorCount++;
if (errorCount >= 10) {
JOptionPane.showMessageDialog(null, "Too many errors in udp class. Please see the logs for more details or visit http://www.gmote.org/faq -- " + e.getMessage());
System.exit(1);
}
}
}
}
private MulticastSocket join(String groupName, int udpPort) {
try {
MulticastSocket msocket;
msocket = new MulticastSocket(udpPort);
if (groupName != null) {
InetAddress group = InetAddress.getByName(groupName);
msocket.joinGroup(group);
}
return msocket;
} catch (IOException e) {
LOGGER.log(Level.SEVERE,e.getMessage(),e);
}
return null;
}
public void receivePacket(MulticastSocket multicastSocket, byte[] inBuffer) {
try {
DatagramPacket packet = new DatagramPacket(inBuffer, inBuffer.length);
// Wait for packet
LOGGER.fine("Waiting for udp packet request on port " + multicastSocket.getLocalPort());
multicastSocket.receive(packet);
if (inBuffer[0] == UdpPacketTypes.SERVICE_DISCOVERY.getId()) {
handleServiceDiscoveryRequest(multicastSocket, packet, inBuffer);
} else if (inBuffer[0] == UdpPacketTypes.MOUSE_MOVE.getId()) {
handleMouseMoveRequest(packet, inBuffer);
} else {
LOGGER.info("Received unrecognized udp packet. Ignoring it.");
//handleLegacyServiceDiscoveryRequest(multicastSocket, packet);
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE,e.getMessage(),e);
}
}
/**
* Handles a mouse move request from the client. In order to accept the mouse
* move event, the client must have an active tcp connection with our server
* (we verify this by matching the ip address).
*
* TODO(mstogaitis): Consider a better security mechanism such as signing the
* mouse packets with the password.
*
* @param packet
* @param data
* a 5 byte packet, byte 0 contains an identifier, bytes 1 and 2
* contain a 'short' describing the XMovement, and bytes 3 and 4
* contain a short describing the YMovement.
*/
private void handleMouseMoveRequest(DatagramPacket packet, byte[] data) {
InetAddress clientAddress = packet.getAddress();
if (isCorrectIp(clientAddress)) {
if (packet.getLength() == 5) {
short xDiff = data[2];
xDiff = (short) ((xDiff << 8) & 0xFF00);
xDiff = (short) (xDiff | (data[1] & 0x00FF));
short yDiff = data[4];
yDiff = (short) ((yDiff << 8 & 0xFF00));
yDiff = (short) (yDiff | (data[3] & 0x00FF));
TrackpadHandler.instance().handleMoveMouseCommand(xDiff, yDiff);
}
} else {
LOGGER
.warning("Received a mouse move request from an ip who is not connected to us: packetIp="
+ clientAddress + " tcpConnectionIp=" + connectedClientIp + ". Ignoring the packet.");
}
}
private static synchronized boolean isCorrectIp(InetAddress clientAddress) {
return clientAddress.equals(connectedClientIp);
}
public static synchronized void setConnectedClientIp(InetAddress clientIp) {
connectedClientIp = clientIp;
}
/**
* Sends a packet to the client identifying our server's name and ip.
*
* @param multicastSocket
* @param packet
* @param data
* Byte 0 is the packet id, bytes 1 to 4 is an int identifying the
* port of the client to which we should send a reply.
* @throws IOException
* @throws UnknownHostException
*/
private void handleServiceDiscoveryRequest(MulticastSocket multicastSocket,
DatagramPacket packet, byte data[]) throws UnknownHostException, IOException {
LOGGER.info("Received an ip request");
if (packet.getLength() == 5) {
int port = data[4] << (24 & 0xFF000000);
port = port | ((data[3] << 16) & 0x00FF0000);
port = port | ((data[2] << 8) & 0x0000FF00);
port = port | (data[1] & 0x000000FF);
sendDiscoveryReply(multicastSocket, packet, port);
}
}
/**
* In version <= 1.2 of the client, we did not use the convention of having
* the first byte of a udp packet identify the packet type. Therefore, for
* backwards compatibility, we'll verify this packet starts with 'gmoteping|'
* which was our original message identifier. If it doesn't we just ignore the
* message.
*
* @param multicastSocket
* @param packet
* @throws IOException
*/
/* private void handleLegacyServiceDiscoveryRequest(MulticastSocket multicastSocket,
DatagramPacket packet) throws IOException {
LOGGER.info("Received an ip request");
// TODO(mstogaitis): Investigate possible security issue here.
String messageRecieved = new String(packet.getData()).trim();
if (messageRecieved.startsWith(CLIENT_PING_PREFIX)) {
String port = messageRecieved.substring(CLIENT_PING_PREFIX.length());
// The client
int remotePort = Integer.parseInt(port);
sendDiscoveryReply(multicastSocket, packet, remotePort);
}
}*/
private void sendDiscoveryReply(MulticastSocket multicastSocket, DatagramPacket packet,
int remotePort) throws UnknownHostException, IOException {
// Reply with all local IPs
byte[] replyBuff;
List<InetAddress> addresses = ServerUtil.findAllLocalIpAddresses(true);
for (InetAddress address : addresses) {
if (!TcpConnectionHandler.instance().isListeningOnAddress(address)) {
LOGGER.warning("Multicase server thread noticed a local ip that we are not listening on. Adding it to the listening pool");
TcpConnectionHandler.instance().addConnectionListener(address);
}
Integer port = PreferredPorts.instance().getPreferredPort(address.getHostAddress());
if (port == null) {
LOGGER.severe("Prefered port is null for connection that should have been added. "
+ address.getHostAddress() + " " + PreferredPorts.instance().getPreferedPorts());
continue;
}
replyBuff = createIpReply(address.getHostAddress(), InetAddress.getLocalHost()
.getHostName(), port);
DatagramPacket replyPacket = new DatagramPacket(replyBuff, replyBuff.length);
replyPacket.setAddress(packet.getAddress());
replyPacket.setPort(remotePort);
LOGGER.info("Sending packet to: " + packet.getAddress());
multicastSocket.send(replyPacket);
}
}
/**
* Create an IP | Hostname reply packet to send to the clients. We can't just
* send the hostName since Android clients run under linux which has problems
* with windows host names.
* @param port
*
* @return
*/
private byte[] createIpReply(String localAddress, String hostName, int port) {
return (localAddress + MulticastClient.FIELD_SEPARATOR + hostName
+ MulticastClient.FIELD_SEPARATOR + port + MulticastClient.FIELD_SEPARATOR + serverUdpListeningPort).getBytes();
}
public static void listenForIpRequests(int udpPort) {
// Ports that the server is listening on.
serverUdpListeningPort = udpPort;
MulticastServerThread multiCon;
if (udpPort != MULTICAST_LISTENING_PORT) {
// The user has chosen to listen for mouse events on a different port than
// service discovery. Make sure we listen on that port as well.
multiCon = new MulticastServerThread(null, udpPort);
new Thread(multiCon, "UdpMouseThread").start();
}
multiCon = new MulticastServerThread(GROUP_NAME, MULTICAST_LISTENING_PORT);
new Thread(multiCon, "MulticastThread").start();
}
}