/*
* Copyright 2004 - 2008 Christian Sprajc, Dennis Waldherr. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder 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.
*
* PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id$
*/
package de.dal33t.powerfolder.net;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeoutException;
import de.dal33t.powerfolder.ConfigurationEntry;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Controller;
import de.dal33t.powerfolder.Member;
import de.dal33t.powerfolder.PFComponent;
import de.dal33t.powerfolder.light.MemberInfo;
import de.dal33t.powerfolder.message.UDTMessage;
import de.dal33t.powerfolder.message.UDTMessage.Type;
import de.dal33t.powerfolder.util.Partitions;
import de.dal33t.powerfolder.util.Range;
import de.dal33t.powerfolder.util.Reject;
import de.dal33t.powerfolder.util.StringUtils;
import de.dal33t.powerfolder.util.net.NetworkUtil;
import de.dal33t.powerfolder.util.net.UDTSocket;
/**
* Listens for incoming UDTMessages and either 1) Processes them if the
* destination is this client or 2) Sends the messages to the destination if
* possible Establishes connections thru UDT sockets. Like with TCP every
* connection requires it's own port!
*
* @author Dennis "Bytekeeper" Waldherr
*/
public class UDTSocketConnectionManager extends PFComponent {
private Partitions<PortSlot> ports;
/**
* Stores received replies. ReplyMonitors are used for locking and waiting
* purposes. Only one Thread needs to be notified, the one waiting for the
* reply.
*/
private ConcurrentMap<MemberInfo, ReplyMonitor> replies = new ConcurrentHashMap<MemberInfo, ReplyMonitor>();
/**
* Constructs a manager for establishing UDT connections.
*
* @param controller
* @param portRange
* the range of possible ports
*/
public UDTSocketConnectionManager(Controller controller, Range portRange) {
super(controller);
ports = new Partitions<PortSlot>(portRange, null);
}
/**
* Creates and initializes an UDT connection. The means used for this are
* similar to that of the RelayedConnectionManager, in that a special
* message is relayed to the target client.
*
* @param destination
* the destination member to connect to
* @return on successfully establishing a connection, null otherwise
* @throws ConnectionException
*/
public ConnectionHandler initRendezvousUDTConnectionHandler(
MemberInfo destination) throws ConnectionException
{
if (!UDTSocket.isSupported()) {
throw new ConnectionException("Missing UDT support!");
}
if (getController().getMySelf().getInfo().equals(destination)) {
throw new ConnectionException(
"Illegal relayed loopback connection detection to myself");
}
// Use relay for UDT stuff too
Member relay = getController().getIOProvider()
.getRelayedConnectionManager().getRelay();
if (relay == null) {
throw new ConnectionException(
"Unable to open relayed connection to " + destination
+ ". No relay found!");
}
if (relay.getInfo().equals(destination)) {
throw new ConnectionException(
"Unable to open relayed connection to relay " + destination);
}
if (!relay.isCompletelyConnected()) {
throw new ConnectionException(
"Unable to open relayed connection to " + destination
+ ". Relay " + relay.getNick() + " not connected.");
}
PortSlot slot = selectPortFor(destination);
if (slot == null) {
throw new ConnectionException("UDT port selection failed!");
}
UDTMessage syn = new UDTMessage(Type.SYN, getController().getMySelf()
.getInfo(), destination, slot.port);
try {
try {
ReplyMonitor repMonitor = new ReplyMonitor();
if (replies.putIfAbsent(destination, repMonitor) != null) {
throw new ConnectionException(
"Already trying to establish connection to "
+ destination);
}
relay.sendMessage(syn);
UDTMessage reply = waitForReply(repMonitor, destination);
switch (reply.getType()) {
case ACK :
logFine("UDT SYN: Trying to connect to " + destination);
ConnectionHandler handler = createAndInitRendezvousUDTSocketConnectionHandler(
getController(), slot.socket, reply.getSource(),
reply.getPort());
logFine("UDT SYN: Successfully connected to "
+ destination);
return handler;
case NACK :
throw new ConnectionException(
"Connection not possible: " + reply);
default :
logFine("UDT SYN: Received invalid reply:" + reply);
throw new ConnectionException("Invalid reply: " + reply);
}
} catch (TimeoutException e) {
logFiner(e);
throw new ConnectionException("Timeout while connecting to "
+ destination, e);
} catch (InterruptedException e) {
logFiner(e);
throw new ConnectionException(
"Interrupted while connecting to " + destination, e);
}
} catch (ConnectionException e) {
// Always remove the entry
replies.remove(destination);
// If we failed, release the slot
releaseSlot(slot.port);
// Don't wait for the GC to collect the socket, close it immediately
if (slot.socket != null && !slot.socket.isClosed()) {
try {
slot.socket.close();
} catch (IOException e1) {
logSevere(e1);
}
}
throw e;
}
}
/**
* Handles UDT messages. Based on the content of the message it might get
* relayed if the destination != mySelf or processed otherwise.
*
* @param sender
* the Member who sent the message
* @param msg
* the message
*/
public void handleUDTMessage(final Member sender, final UDTMessage msg) {
// Are we targeted ?
if (msg.getDestination().matches(getController().getMySelf())) {
handleMessageForMyself(sender, msg);
} else {
relayMessage(sender, msg);
}
}
/**
* Returns a port to use for the given destination. Each connection requires
* it's own port on both sides. The returned slot contains an UDTSocket
* which is already bound to the selected port.
*
* @param destination
* @return the slot
*/
public PortSlot selectPortFor(MemberInfo destination) {
Range res = null;
// Try to bind port now to avoid surprises later
PortSlot slot = new PortSlot(destination);
slot.socket = new UDTSocket();
try {
NetworkUtil.setupSocket(slot.socket, destination
.getConnectAddress(), getController());
} catch (IOException e1) {
logSevere(e1);
}
while (true) {
synchronized (this) {
res = ports.search(ports.getPartionedRange(), null);
if (res != null) {
// Lock the port. Either this gets overridden below or it
// stays locked for good.
ports.insert(Range.getRangeByNumbers(res.getStart(), res
.getStart()), PortSlot.LOCKED);
}
}
if (res == null) {
logSevere("No further usable ports for UDT connections!");
try {
slot.socket.close();
} catch (IOException e) {
logSevere(e);
}
return null;
}
slot.port = (int) res.getStart();
try {
String cfgBindAddr = ConfigurationEntry.NET_BIND_ADDRESS
.getValue(getController());
InetSocketAddress bindAddr;
if (!StringUtils.isEmpty(cfgBindAddr)) {
bindAddr = new InetSocketAddress(cfgBindAddr, slot.port);
} else {
bindAddr = new InetSocketAddress(slot.port);
}
slot.socket.bind(bindAddr);
break;
} catch (IOException e) {
logFiner(e);
}
}
synchronized (this) {
ports.insert(Range
.getRangeByNumbers(res.getStart(), res.getStart()), slot);
}
return slot;
}
/**
* Frees a slot taken by selectPortFor
*
* @param port
* the port slot to free
*/
public synchronized void releaseSlot(int port) {
ports.insert(Range.getRangeByLength(port, 1), null);
}
// Internal methods *******************************************************
/**
* Creates an initialized connection handler for a UDT socket based on UDP
* connection.
*
* @param controller
* the controller.
* @param socket
* the UDT socket
* @param port
* @param dest
* @return the connection handler for basic IO connection.
* @throws ConnectionException
*/
private AbstractUDTSocketConnectionHandler createAndInitRendezvousUDTSocketConnectionHandler(
Controller controller, UDTSocket socket, MemberInfo dest, int port)
throws ConnectionException
{
MemberInfo remoteInfo = dest.getNode(getController(), true).getInfo();
InetSocketAddress destination = new InetSocketAddress(remoteInfo
.getConnectAddress().getAddress(), port);
AbstractUDTSocketConnectionHandler conHan = null;
try {
socket.setSoRendezvous(true);
if (isFiner()) {
logFiner("UDT connect to " + destination);
}
socket.connect(destination);
if (isFiner()) {
logFiner("UDT socket is connected to " + destination);
}
conHan = getController().getIOProvider()
.getConnectionHandlerFactory()
.createAndInitUDTSocketConnectionHandler(socket);
if (isFiner()) {
logFiner("Is connected? " + conHan.isConnected() + " : "
+ socket.getRemoteAddress());
}
} catch (ConnectionException e) {
try {
socket.close();
} catch (IOException ex) {
}
throw e;
} catch (RuntimeException e) {
try {
socket.close();
} catch (IOException ex) {
}
if (conHan != null) {
conHan.shutdown();
}
throw e;
} catch (IOException e) {
try {
socket.close();
} catch (IOException ex) {
}
throw new ConnectionException(
"IOException while opening UDT connection: " + e, e);
}
return conHan;
}
private void relayMessage(final Member sender, final UDTMessage msg) {
if (isFiner()) {
logFiner("Relaying UDT message: " + msg);
}
// Relay message
Member dMember = msg.getDestination().getNode(getController(), false);
if (dMember == null || !dMember.isCompletelyConnected()) {
UDTMessage failedMsg = new UDTMessage(Type.NACK, msg
.getDestination(), msg.getSource(), -1);
sender.sendMessagesAsynchron(failedMsg);
return;
}
dMember.sendMessagesAsynchron(msg);
}
private void handleMessageForMyself(final Member sender,
final UDTMessage msg)
{
if (isFiner()) {
logFiner("Received UDT message for me: " + msg);
logFiner("Replies: " + replies.size());
}
if (!UDTSocket.isSupported()) {
logFiner("UDT sockets not supported on this platform.");
return;
}
switch (msg.getType()) {
case SYN :
// Check if we allow NAT traversal
if (getController().getIOProvider()
.getConnectionHandlerFactory().useUDTConnections())
{
getController().getIOProvider().startIO(
new ConnectionInitializer(sender, msg));
} else {
UDTMessage nack = new UDTMessage(Type.NACK, getController()
.getMySelf().getInfo(), sender.getInfo(), -1);
// Try to send NACK, if it doesn't work - we don't care,
// it'll timeout remotely.
sender.sendMessagesAsynchron(nack);
}
break;
case ACK :
case NACK :
ReplyMonitor repMon = replies.get(msg.getSource());
if (repMon == null) {
logWarning("Received a reply for " + msg.getSource()
+ ", although no connection was requested!");
break;
}
synchronized (repMon) {
if (repMon.msg != null) {
logSevere("Relay message error: Received more than one SYN reply!");
// If that happens, let's hope the "newer" message
// is the "better".
}
repMon.msg = msg;
repMon.notify();
}
break;
}
}
private UDTMessage waitForReply(ReplyMonitor monitor, MemberInfo destination)
throws TimeoutException, InterruptedException
{
synchronized (monitor) {
try {
// Check if we already got a reply
if (monitor.msg != null) {
return monitor.msg;
}
monitor.wait(Constants.TO_UDT_CONNECTION);
if (monitor.msg != null) {
return monitor.msg;
}
throw new TimeoutException();
} finally {
if (isFiner()) {
logFiner("waitForReply remaining entries: "
+ replies.size());
}
// Always remove the entry
replies.remove(destination);
}
}
}
// Inner classes **********************************************************
private final class ConnectionInitializer implements Runnable {
private final Member sender;
private final UDTMessage msg;
private ConnectionInitializer(Member sender, UDTMessage msg) {
Reject.ifNull(sender, "Sender/Relay");
this.sender = sender;
this.msg = msg;
}
public void run() {
PortSlot slot = selectPortFor(sender.getInfo());
if (slot == null) {
logSevere("UDT port selection failed.");
try {
sender.sendMessage(new UDTMessage(Type.NACK,
getController().getMySelf().getInfo(), msg.getSource(),
-1));
} catch (ConnectionException e) {
logSevere(e);
}
return;
}
try {
sender.sendMessage(new UDTMessage(Type.ACK, getController()
.getMySelf().getInfo(), msg.getSource(), slot.port));
ConnectionHandler handler = null;
try {
logFine("UDT ACK: Trying to connect...");
handler = createAndInitRendezvousUDTSocketConnectionHandler(
getController(), slot.socket, msg.getSource(), msg
.getPort());
getController().getNodeManager().acceptConnection(handler);
logFine("UDT ACK: Successfully connected!");
} catch (ConnectionException e) {
if (handler != null)
handler.shutdown();
throw e;
}
} catch (ConnectionException e) {
// If we failed, release the slot
releaseSlot(slot.port);
// Don't wait for the GC to collect the
// socket, close it immediately
if (slot.socket != null && !slot.socket.isClosed()) {
try {
slot.socket.close();
} catch (IOException e1) {
logSevere(e1);
}
}
logFiner("Unable to connect (UDT) to " + msg.getSource() + ": "
+ e, e);
}
}
}
/**
* Simple class representing a port, a socket and the target member.
*/
private static class PortSlot {
/** If a port is locked - it's pretty much dead (unusable for PF) */
public static final PortSlot LOCKED = new PortSlot();
private MemberInfo member;
private UDTSocket socket;
private int port;
public PortSlot(MemberInfo destination) {
member = destination;
}
private PortSlot() {
// for locked port-slot
}
public MemberInfo getMember() {
return member;
}
public UDTSocket getSocket() {
return socket;
}
public int getPort() {
return port;
}
}
private static class ReplyMonitor {
public UDTMessage msg;
}
}