//******************************************************************
//******************************************************************
//********** ANts Peer To Peer Sources *************
//
// ANts P2P realizes a third generation P2P net. It protects your
// privacy while you are connected and makes you not trackable, hiding
// your identity (ip) and crypting everything you are sending/receiving
// from others.
// Copyright (C) 2004 Roberto Rossi
// 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 2
// 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, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
package ants.p2p.utils.net;
import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Enumeration;
import java.util.Random;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cybergarage.upnp.Action;
import org.cybergarage.upnp.Argument;
import org.cybergarage.upnp.ControlPoint;
import org.cybergarage.upnp.Device;
import org.cybergarage.upnp.DeviceList;
import org.cybergarage.upnp.Service;
import org.cybergarage.upnp.device.DeviceChangeListener;
import org.cybergarage.upnp.control.*;
import ants.p2p.gui.FrameAnt;
import org.apache.log4j.*;
import org.cybergarage.upnp.StateVariable;
import java.io.File;
import org.cybergarage.upnp.device.InvalidDescriptionException;
import org.cybergarage.upnp.ServiceList;
import ants.p2p.filesharing.WarriorAnt;
/**
*
* According to the UPnP Standards, Internet Gateway Devices must have a
* specific hierarchy. The parts of that hierarchy that we care about are:
*
* Device: urn:schemas-upnp-org:device:InternetGatewayDevice:1
* SubDevice: urn:schemas-upnp-org:device:WANDevice:1
* SubDevice: urn:schemas-upnp-org:device:WANConnectionDevice:1
* Service: urn:schemas-upnp-org:service:WANIPConnection:1
*
* Every port mapping is a tuple of:
* - External address ("" is wildcard)
* - External port
* - Internal address
* - Internal port
* - Protocol (TCP|UDP)
* - Description
*
* Port mappings can be removed, but that is a blocking network operation which will
* slow down the shutdown process of Limewire. It is safe to let port mappings persist
* between limewire sessions. In the meantime however, the NAT may assign a different
* ip address to the local node. That's why we need to find any previous mappings
* the node has created and update them with our new address. In order to uniquely
* distinguish which mappings were made by us, we put part of our client GUID in the
* description field.
*
* For the TCP mapping, we use the following description: "ANts/TCP:<cliengGUID>"
*
* NOTES:
*
* Not all NATs support mappings with different external port and internal ports. Therefore
* if we were unable to map our desired port but were able to map another one, we should
* pass this information on to Acceptor.
*
* Some buggy NATs do not distinguish mappings by the Protocol field. Therefore
* we first map the UDP port, and then the TCP port since it is more important should the
* first mapping get overwritten.
*
* The cyberlink library uses an internal thread that tries to discover any UPnP devices.
* After we discover a router or give up on trying to, we should call stop().
*
*/
public class UPnPManager extends ControlPoint implements DeviceChangeListener, ActionListener, QueryListener {
private static Logger _logger = Logger.getLogger(UPnPManager.class.getName());
private final static String DESCRIPTION_FILE_NAME = WarriorAnt.workingPath+"upnpdescriptor/description.xml";
private final static String PRESENTATION_URI = "http://antsp2p.sourceforge.net";
public final static String ANTS_DEVICE_TYPE = "urn:schemas-upnp-org:device:antsp2p:1";
public final static String ANTS_LAN_ADDRESS_SERVICE_TYPE = "urn:schemas-upnp-org:service:address:1";
public final static String ANTS_LAN_ADDRESS_ACTION_TYPE = "GetLANAddress";
private StateVariable serverInfo;
private Device antsDev = null;
/** some schemas */
private static final String ROUTER_DEVICE=
"urn:schemas-upnp-org:device:InternetGatewayDevice:1";
private static final String WAN_DEVICE =
"urn:schemas-upnp-org:device:WANDevice:1";
private static final String WANCON_DEVICE=
"urn:schemas-upnp-org:device:WANConnectionDevice:1";
private static final String SERVICE_TYPE =
"urn:schemas-upnp-org:service:WANIPConnection:1";
/** prefixes and a suffix for the descriptions of our TCP and UDP mappings */
private static final String TCP_PREFIX = "ANtsTCP";
//private static final String UDP_PREFIX = "ANtsUDP";
private String _guidSuffix;
private static UPnPManager INSTANCE = null;
public static synchronized UPnPManager instance() {
if(INSTANCE == null){
UPnPDescriptor.createDirectoryStructure();
return INSTANCE = new UPnPManager();
}else{
return INSTANCE;
}
}
/**
* the router we have and the sub-device necessary for port mapping
* LOCKING: this
*/
private Device _router;
/**
* The port-mapping service we'll use. LOCKING: this
*/
private Service _service;
/** The tcp and udp mappings created this session */
private Mapping _tcp;//, _udp;
private UPnPManager(){
super();
_logger.info("Starting UPnP Service Manager.");
try{
addDeviceChangeListener(this);
start();
}catch(Exception bad) {
bad.printStackTrace();
}
}
public Device getDevice(){
return this.antsDev;
}
public void setCurrentLanAddress(ants.p2p.query.ServerInfo si){
try{
if (si == null || this.serverInfo == null)
return;
this.serverInfo.setValue(si.toString());
}catch(Exception e){
_logger.error("Error in setting UPnP LAN address", e);
}
}
public ants.p2p.query.ServerInfo getCurrentLanAddress(){
try{
if(this.serverInfo == null) return null;
String[] addressPort = this.serverInfo.getValue().split(":");
ants.p2p.query.ServerInfo si = new ants.p2p.query.ServerInfo("", addressPort[0], new Integer(addressPort[1]),"");
return si;
}catch(Exception e){
_logger.error("Error in retrieving UPnP LAN address", e);
return null;
}
}
public void startDevice(){
stopDevice();
try {
antsDev = new Device(DESCRIPTION_FILE_NAME);
Action getTimeAction = antsDev.getAction("GetLANAddress");
getTimeAction.setActionListener(this);
ServiceList serviceList = antsDev.getServiceList();
Service service = serviceList.getService(0);
service.setQueryListener(this);
serverInfo = antsDev.getStateVariable("LANAddress");
antsDev.setLeaseTime(60);
antsDev.start();
}
catch (InvalidDescriptionException e) {
_logger.error("Invalid description file for ANtsP2P UPnP Service", e);
}
}
public void stopDevice(){
if(antsDev != null) {
antsDev.byebye();
antsDev.stop();
this.antsDev = null;
this.serverInfo = null;
}
}
public boolean queryControlReceived(StateVariable stateVar){
//MUST SET THE LAN ADDRESS HERE (SERVER INFO)!!!!
stateVar.setValue(serverInfo.getValue());
return true;
}
public boolean actionControlReceived(Action action){
String actionName = action.getName();
if (actionName.equals("GetLANAddress") == true) {
Argument timeArg = action.getArgument("LANAddress");
timeArg.setValue(serverInfo.getValue());
return true;
}
return false;
}
public void update(String newValue) {
serverInfo.setValue(newValue);
}
/**
* @return whether we are behind an UPnP-enabled NAT/router
*/
public synchronized boolean isNATPresent() {
return _router != null && _service != null;
}
/**
* @return whether we have created mappings this session
*/
public /*synchronized*/ boolean mappingsExist() {
return _tcp != null /*&& _udp != null*/;
}
/**
* @return the external address the NAT thinks we have. Blocking.
* null if we can't find it.
*/
public InetAddress getNATAddress() throws UnknownHostException {
Action getIP;
synchronized(this) {
if (!isNATPresent())
return null;
getIP = _service.getAction("GetExternalIPAddress");
}
if(getIP == null) {
_logger.info("Couldn't find GetExternalIPAddress action!");
return null;
}
if (!getIP.postControlAction()) {
_logger.info("couldn't get our external address");
return null;
}
Argument ret = getIP.getOutputArgumentList().getArgument("NewExternalIPAddress");
return InetAddress.getByName(ret.getValue());
}
/**
* this method will be called when we discover a UPnP device.
*/
public synchronized void deviceAdded(Device dev) {
_logger.info("Device added: " + dev.getFriendlyName());
// we've found what we need
if (_service != null && _router != null) {
_logger.info("we already have a router");
return;
}
// did we find a router?
if (dev.getDeviceType().equals(ROUTER_DEVICE) && dev.isRootDevice())
_router = dev;
if (_router == null) {
_logger.info("didn't get router device");
return;
}
discoverService();
// did we find the service we need?
if (_service == null) {
_logger.info("couldn't find service");
_router=null;
} else {
_logger.info("Found service, router: " + _router.getFriendlyName() + ", service: " + _service);
//stop();
}
}
/**
* Traverses the structure of the router device looking for
* the port mapping service.
*/
private void discoverService() {
for (Iterator iter = _router.getDeviceList().iterator();iter.hasNext();) {
Device current = (Device)iter.next();
if (!current.getDeviceType().equals(WAN_DEVICE))
continue;
DeviceList l = current.getDeviceList();
_logger.info("found "+current.getDeviceType()+", size: "+l.size() + ", on: " + current.getFriendlyName());
for (int i=0;i<current.getDeviceList().size();i++) {
Device current2 = l.getDevice(i);
if (!current2.getDeviceType().equals(WANCON_DEVICE))
continue;
_logger.info("found "+current2.getDeviceType() + ", on: " + current2.getFriendlyName());
_service = current2.getService(SERVICE_TYPE);
return;
}
}
}
/**
* adds a mapping on the router to the specified port
* @return the external port that was actually mapped. 0 if failed
*/
public int mapPort(String address, int port) {
_logger.info("Attempting to map port: " + port);
Random gen=null;
String localAddress = address;
int localPort = port;
// try adding new mappings with the same port
/*Mapping udp = new Mapping("",
port,
localAddress,
localPort,
"UDP",
UDP_PREFIX + getGUIDSuffix());
// add udp first in case it gets overwritten.
// if we can't add, update or find an appropriate port
// give up after 20 tries
int tries = 20;
while (!addMapping(udp)) {
if (tries<0)
break;
tries--;
// try a random port
if (gen == null)
gen = new Random();
port = gen.nextInt(50000)+2000;
udp = new Mapping("",
port,
localAddress,
localPort,
"UDP",
UDP_PREFIX + getGUIDSuffix());
}
if (tries < 0) {
_logger.info("couldn't map a port :(");
return 0;
}*/
// at this stage, the variable port will point to the port the UDP mapping
// got mapped to. Since we have to have the same port for UDP and tcp,
// we can't afford to change the port here. So if mapping to this port on tcp
// fails, we give up and clean up the udp mapping.
Mapping tcp = new Mapping("",
port,
localAddress,
localPort,
"TCP",
TCP_PREFIX + getGUIDSuffix());
if (!addMapping(tcp)) {
_logger.info(" couldn't map tcp to whatever udp was mapped. cleaning up...");
port = 0;
tcp = null;
//udp = null;
}
// save a ref to the mappings
synchronized(this) {
_tcp = tcp;
//_udp = udp;
}
// we're good - start a thread to clean up any potentially stale mappings
Thread staleCleaner = new Thread(new StaleCleaner());
staleCleaner.setDaemon(true);
staleCleaner.setName("Stale Mapping Cleaner");
staleCleaner.start();
return port;
}
/**
* @param m Port mapping to send to the NAT
* @return the error code
*/
private boolean addMapping(Mapping m) {
_logger.info("adding "+m);
Action add;
synchronized(this) {
add = _service.getAction("AddPortMapping");
}
if(add == null) {
_logger.info("Couldn't find AddPortMapping action!");
return false;
}
add.setArgumentValue("NewRemoteHost",m._externalAddress);
add.setArgumentValue("NewExternalPort",m._externalPort);
add.setArgumentValue("NewInternalClient",m._internalAddress);
add.setArgumentValue("NewInternalPort",m._internalPort);
add.setArgumentValue("NewProtocol",m._protocol);
add.setArgumentValue("NewPortMappingDescription",m._description);
add.setArgumentValue("NewEnabled","1");
add.setArgumentValue("NewLeaseDuration",0);
boolean success = add.postControlAction();
_logger.info("Post succeeded: " + success);
return success;
}
/**
* @param m the mapping to remove from the NAT
* @return whether it worked or not
*/
private boolean removeMapping(Mapping m) {
_logger.info("removing "+m);
Action remove;
synchronized(this) {
remove = _service.getAction("DeletePortMapping");
}
if(remove == null) {
_logger.info("Couldn't find DeletePortMapping action!");
return false;
}
remove.setArgumentValue("NewRemoteHost",m._externalAddress);
remove.setArgumentValue("NewExternalPort",m._externalPort);
remove.setArgumentValue("NewProtocol",m._protocol);
boolean success = remove.postControlAction();
_logger.info("Remove succeeded: " + success);
return success;
}
/**
* schedules a shutdown hook which will clear the mappings created
* this session.
*/
public void clearMappingsOnShutdown() {
final Mapping tcp/*, udp*/;
synchronized(this) {
tcp = _tcp;
//udp = _udp;
}
final Thread cleaner = new Thread() {
public void run() {
_logger.info("start cleaning");
removeMapping(tcp);
//removeMapping(udp);
_logger.info("done cleaning");
}
};
Thread waiter = new Thread() {
public void run() {
try{
_logger.info("waiting for UPnP cleaners to finish");
cleaner.join(30000); // wait at most 30 seconds.
}catch(InterruptedException ignored){}
_logger.info("UPnP cleaners done");
}
};
waiter.setName("shutdown mapping waiter");
try {
Runtime.getRuntime().addShutdownHook(waiter);
} catch (IllegalStateException ignored){}
cleaner.setName("shutdown mapping cleaner");
cleaner.setDaemon(true);
cleaner.start();
Thread.yield(); // let it start.
}
public void finalize() {
stop();
}
private synchronized String getGUIDSuffix() {
if (_guidSuffix == null)
_guidSuffix = FrameAnt.getInstance(null).getGuiAnt().getConnectionAntPanel().getWarriorAnt().getIdent().substring(0,10);
return _guidSuffix;
}
/**
* stub
*/
public void deviceRemoved(Device dev) {}
/**
* @return A non-loopback IPv4 address of a network interface on the
* local host.
* @throws UnknownHostException
*/
public static InetAddress getLocalAddress()
throws UnknownHostException {
InetAddress addr = InetAddress.getLocalHost();
if (addr instanceof Inet4Address && !addr.isLoopbackAddress())
return addr;
try {
Enumeration interfaces =
NetworkInterface.getNetworkInterfaces();
if (interfaces != null) {
while (interfaces.hasMoreElements()) {
Enumeration addresses =
((NetworkInterface)interfaces.nextElement()).getInetAddresses();
while (addresses.hasMoreElements()) {
addr = (InetAddress) addresses.nextElement();
if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
return addr;
}
}
}
}
} catch (SocketException se) {}
throw new UnknownHostException(
"localhost has no interface with a non-loopback IPv4 address");
}
private final class Mapping {
public final String _externalAddress;
public final int _externalPort;
public final String _internalAddress;
public final int _internalPort;
public final String _protocol,_description;
// network constructor
public Mapping(String externalAddress,String externalPort,
String internalAddress, String internalPort,
String protocol, String description) throws NumberFormatException{
_externalAddress=externalAddress;
_externalPort=Integer.parseInt(externalPort);
_internalAddress=internalAddress;
_internalPort=Integer.parseInt(internalPort);
_protocol=protocol;
_description=description;
}
// internal constructor
public Mapping(String externalAddress,int externalPort,
String internalAddress, int internalPort,
String protocol, String description) {
_externalAddress=externalAddress;
_externalPort=externalPort;
_internalAddress=internalAddress;
_internalPort=internalPort;
_protocol=protocol;
_description=description;
}
public String toString() {
return _externalAddress+":"+_externalPort+"->"+_internalAddress+":"+_internalPort+
"@"+_protocol+" desc: "+_description;
}
}
/**
* This thread reads all the existing mappings on the NAT and if it finds
* a mapping which appears to be created by us but points to a different
* address (i.e. is stale) it removes the mapping.
*
* It can take several minutes to finish, depending on how many mappings there are.
*/
private class StaleCleaner implements Runnable {
// TODO: remove
private String list(java.util.List l) {
String s = "";
for(Iterator i = l.iterator(); i.hasNext(); ) {
Argument next = (Argument)i.next();
s += next.getName() + "->" + next.getValue() + ", ";
}
return s;
}
public void run() {
_logger.info("Looking for stale mappings...");
Set mappings = new HashSet();
Action getGeneric;
synchronized(UPnPManager.this) {
getGeneric = _service.getAction("GetGenericPortMappingEntry");
}
if(getGeneric == null) {
_logger.info("Couldn't find GetGenericPortMappingEntry action!");
return;
}
// get all the mappings
try {
for (int i=0;;i++) {
getGeneric.setArgumentValue("NewPortMappingIndex",i);
_logger.info("Stale Iteration: " + i + ", generic.input: " + list(getGeneric.getInputArgumentList()) + ", generic.output: " + list(getGeneric.getOutputArgumentList()));
if (!getGeneric.postControlAction())
break;
mappings.add(new Mapping(
getGeneric.getArgumentValue("NewRemoteHost"),
getGeneric.getArgumentValue("NewExternalPort"),
getGeneric.getArgumentValue("NewInternalClient"),
getGeneric.getArgumentValue("NewInternalPort"),
getGeneric.getArgumentValue("NewProtocol"),
getGeneric.getArgumentValue("NewPortMappingDescription")));
// TODO: erase output arguments.
}
}catch(NumberFormatException bad) {
_logger.error("NFE reading mappings!", bad);
//router broke.. can't do anything.
return;
}
_logger.info("Stale cleaner found "+mappings.size()+ " total mappings");
// iterate and clean up
for (Iterator iter = mappings.iterator();iter.hasNext();) {
Mapping current = (Mapping)iter.next();
_logger.info("Analyzing: " + current);
if(current._description == null)
continue;
// does it have our description?
if (current._description.equals(TCP_PREFIX+getGUIDSuffix())/* ||
current._description.equals(UDP_PREFIX+getGUIDSuffix())*/) {
// is it not the same as the mappings we created this session?
synchronized(this) {
if (_tcp != null && /*_udp != null &&*/
current._externalPort == _tcp._externalPort &&
current._internalAddress.equals(_tcp._internalAddress) &&
current._internalPort == _tcp._internalPort)
continue;
}
// remove it.
_logger.info("mapping "+current+" appears to be stale");
removeMapping(current);
}
}
}
}
}