/*
* UDPhandler.java
*
* Created on 5. November 2005, 23:20
*
* handles incoming UDP packets(search results)
*/
package uc;
import helpers.GH;
import helpers.PreferenceChangedAdapter;
import java.io.IOException;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import logger.LoggerFactory;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.Platform;
import uc.crypto.UDPEncryption;
import uc.protocols.DCProtocol;
import uc.protocols.IConnectionDebugger;
import uc.protocols.Socks;
import uc.protocols.DCPacketReceiver.ADCReceiver;
import uc.protocols.DCPacketReceiver.NMDCReceiver;
/**
*
* Receives UDP signals and provides facility to send UDP packets
*
* @author Quicksilver
*/
public class UDPhandler implements IUDPHandler {
private static final Logger logger = LoggerFactory.make();
/**
* decoders and encoders for the charsets
*/
private final CharsetEncoder nmdcencoder,
adcencoder;
private final CopyOnWriteArrayList<byte[]> keysActive = new CopyOnWriteArrayList<byte[]>();
private final CopyOnWriteArraySet<IConnectionDebugger> debuggers =
new CopyOnWriteArraySet<IConnectionDebugger>();
/**
* communication variable to signal port changed in the settings
*/
private volatile boolean portchanged;
private volatile int port;
private Thread udpThread;
private volatile boolean running = true;
private final PreferenceChangedAdapter pca;
private volatile DatagramChannel datagramChannel = null;
private static final int UDPMAXPAYLOAD = 65536;
private ByteBuffer packet = ByteBuffer.allocate(UDPMAXPAYLOAD/4); //larger than 16KiB won'T arrive anyway..
private PacketReceiver[] receivers = new PacketReceiver[255];
private final DCClient dcc;
/** will handle the incoming Searches UDP Port */
public UDPhandler(DCClient dcc) {
this.dcc = dcc;
nmdcencoder = DCProtocol.NMDC_CHARSET.newEncoder();
nmdcencoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
nmdcencoder.onMalformedInput(CodingErrorAction.REPLACE);
adcencoder = DCProtocol.ADC_CHARSET.newEncoder();
adcencoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
adcencoder.onMalformedInput(CodingErrorAction.REPLACE);
//listener for port changes in the settings
pca = new PreferenceChangedAdapter(PI.get(),PI.udpPort,PI.passive) {
@Override
public void preferenceChanged(String preference, String oldValue,String newValue) {
portchanged = true;
GH.close(datagramChannel);
}
};
register((int)'$', new NMDCReceiver(dcc));
register((int)'U', new ADCReceiver(dcc));
}
/**
* opens the UDP socket..
* @throws IOException - may be error because port is in use..
*/
private void open() throws IOException {
try {
datagramChannel = DatagramChannel.open();
datagramChannel.socket().bind(PI.getBoolean(PI.passive)? null : new InetSocketAddress(port));
datagramChannel.configureBlocking(true);
dcc.logEvent(String.format(LanguageKeys.UDPStarted, port));// "UDP handler gone up on Port: "+port);
} catch (SecurityException se) {
if (port <= 1024 && (Platform.getOS().equals(Platform.OS_MACOSX) || Platform.getOS().equals(Platform.OS_LINUX)) ) {
throw new IOException(String.format(LanguageKeys.UNIXPortWarning, Platform.getOS()),se); // "Unix based OS need root rights to use ports lower than 1024!",se );
}
throw new IOException(se);
}
}
/* (non-Javadoc)
* @see uc.IUDPHandler#start()
*/
public void start() {
udpThread = new Thread(new Runnable() {
public void run() {
runUDP();
}
},"UDP-handler");
udpThread.start();
pca.reregister();
}
/* (non-Javadoc)
* @see uc.IUDPHandler#stop()
*/
public void stop() {
running = false;
GH.close(datagramChannel);
pca.dispose();
}
/**
* runs the channel endlessly loops for receiving packets
* and provides payloads of the packets to receivedPacket()
*
*/
private void runUDP() {
while (running) {
try {
portchanged = false;
port = PI.getInt(PI.udpPort);
open();
while (!portchanged && datagramChannel.isOpen()) {
packet.clear();
SocketAddress from = datagramChannel.receive(packet);
if (from instanceof InetSocketAddress) {
packet.flip();
receivedPacket((InetSocketAddress)from, packet,true);
} else {
logger.debug("not a InetSocketAddress received: "+from);
}
}
} catch (IOException ioe) {
if (!portchanged && running) {
logger.warn(ioe,ioe);
} else {
logger.debug(ioe,ioe);
}
GH.sleep(5000);
} finally {
GH.close(datagramChannel);
}
}
}
/**
* checks if a packet is nmdc or adc,
* decodes it accordingly and then gives it to the matching method
*
* @param from - address of sender
* @param packet - buffer containing the payload
* @param possiblyEncrypted - true if the packet might possibly be encrypted
* @throws CharacterCodingException
*/
protected void receivedPacket(InetSocketAddress from, ByteBuffer packet,boolean possiblyEncrypted) {
//charBuffer.clear();
dcc.getDefaultIdentity().getConnectionDeterminator().udpPacketReceived(from);
if (packet.hasRemaining()) {
PacketReceiver r = receivers[(int)(packet.get(0) & 0xff)];
if (r != null && r.matches(packet.get(1), packet.get(2), packet.get(3))) {
r.packetReceived(packet, from);
// for (IConnectionDebugger debugger:debuggers) {
// debugger.receivedCommand(commandHandler, t, command)
// }
} else if (possiblyEncrypted && packet.remaining() > UDPEncryption.KEYLENGTH*2) {
byte[] encrypted = new byte[packet.remaining()];
packet.get(encrypted);
byte[] decrypted = UDPEncryption.decryptMessage(encrypted, keysActive);
if (decrypted != null) {
receivedPacket(from,ByteBuffer.wrap(decrypted),false);
} else {
if (Platform.inDevelopmentMode()) {
logger.warn("unknown PacketReceived: "+ new String(packet.array())+" "+possiblyEncrypted);
}
}
}
}
}
// /* (non-Javadoc)
// * @see uc.IUDPHandler#sendSearchResultsBack(java.util.Set, uc.protocols.hub.Hub, java.net.InetSocketAddress)
// */
// public void sendSearchResultsBack(Set<SearchResult> srs,Hub hub ,InetSocketAddress target) {
// logger.debug("sending search results back");
// CharBuffer charBuffer = CharBuffer.allocate(UDPMAXPAYLOAD/4);
// try {
// for (SearchResult sr: srs) {
// String command = hub.getUDPSRPacket(sr);
// charBuffer.put(command);
// charBuffer.flip();
//
// if (hub.isNMDC() && !hub.getCharset().equals(DCProtocol.NMDCCHARSET)) {
// sendPacket(hub.getCharset().encode(charBuffer),target); //use overridden charsets
// } else {
// CharsetEncoder encoder = hub.isNMDC()? nmdcencoder:adcencoder;
// synchronized(encoder) {
// sendPacket(encoder.encode(charBuffer),target);
// }
// }
// charBuffer.clear();
// }
//
// } catch (CharacterCodingException cee) {
// logger.warn(cee,cee);
// }
// }
/* (non-Javadoc)
* @see uc.IUDPHandler#sendPacket(java.nio.ByteBuffer, java.net.InetSocketAddress)
*/
public void sendPacket(ByteBuffer packet,InetSocketAddress target ) {
if (!Socks.isEnabled()) {
sendPacketIgnoreProxy(packet, target);
} else {
// ... UDP Relay... though not really needed..
//as seems not to work and is common in DC++ client to not answer with udp..
}
}
/* (non-Javadoc)
* @see uc.IUDPHandler#sendPacketIgnoreProxy(java.nio.ByteBuffer, java.net.InetSocketAddress)
*/
public void sendPacketIgnoreProxy(ByteBuffer packet,InetSocketAddress target ){
if (0 < target.getPort() && target.getPort() <= 65535) {
logger.debug("sending udp packet: "+target+ " "+ packet);
try {
int i = datagramChannel.send(packet, target);
logger.debug("sent bytes: "+i);
} catch(BindException be) {
logger.debug("Packet could not be send: "+be,be);
} catch (IOException ioe) {
logger.warn(ioe+" packet: "+target+" "+new String(packet.array()),ioe);
}
}
}
/* (non-Javadoc)
* @see uc.IUDPHandler#register(int, uc.UDPhandler.PacketReceiver)
*/
public final void register(int firstByte,PacketReceiver receiver ) {
if (firstByte < 0 || firstByte > 255 || receiver == null) {
throw new IllegalArgumentException();
}
if (receivers[firstByte] == null) {
receivers[firstByte] = receiver;
} else {
throw new IllegalArgumentException("Already registered a receiver for that byte");
}
}
/* (non-Javadoc)
* @see uc.IUDPHandler#unregister(int, uc.UDPhandler.PacketReceiver)
*/
public void unregister(int firstByte,PacketReceiver receiver) {
if (firstByte < 0 || firstByte > 255 || receiver == null) {
throw new IllegalArgumentException();
}
if (receivers[firstByte] != null) {
if (receivers[firstByte].equals(receiver)) {
receivers[firstByte] = null;
} else {
throw new IllegalArgumentException("Not registered on that byte");
}
}
}
/* (non-Javadoc)
* @see uc.IUDPHandler#getPort()
*/
public int getPort() {
return port;
}
/**
*
* tokens must be told to UDP handler for decryption
* @param token the token used..
*/
public void addKeyExpected(byte[] key) {
if (key.length != UDPEncryption.KEYLENGTH) {
throw new IllegalArgumentException();
}
//byte[] key = UDPEncryption.tokenStringToKey(token);
keysActive.add(0,key);
if (keysActive.size() > 10) {
keysActive.remove(keysActive.size()-1);
}
}
public void registerConnectionDebugger(IConnectionDebugger debugger) {
debuggers.add(debugger);
}
public void unregisterConnectionDebugger(IConnectionDebugger debugger) {
debuggers.remove(debugger);
}
public static interface PacketReceiver {
/**
* signals that a packet was received.
* on returning from the method call the ByteBuffer may
* no longer be used by the called object. As it will be used for other packets.
*
* @param packet - the packet received from the user..
* @param source - where it came from..
*
*/
void packetReceived(ByteBuffer packet,InetSocketAddress source);
/**
* while byte zero is used to discriminate against messages .. -> this is further used for faster
* check than with encryption..
* gets byte 1,2 and 3 against the Handler
* @param one
* @param two
* @param three
* @return
*/
boolean matches(byte one,byte two,byte three);
}
}