/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package net.i2p.router.transport;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.Addresses;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.Translate;
import org.cybergarage.upnp.Action;
import org.cybergarage.upnp.ActionList;
import org.cybergarage.upnp.Argument;
import org.cybergarage.upnp.ArgumentList;
import org.cybergarage.upnp.ControlPoint;
import org.cybergarage.upnp.Device;
import org.cybergarage.upnp.DeviceList;
import org.cybergarage.upnp.Service;
import org.cybergarage.upnp.ServiceList;
import org.cybergarage.upnp.ServiceStateTable;
import org.cybergarage.upnp.StateVariable;
import org.cybergarage.upnp.UPnPStatus;
import org.cybergarage.upnp.device.DeviceChangeListener;
import org.cybergarage.upnp.event.EventListener;
import org.freenetproject.DetectedIP;
import org.freenetproject.ForwardPort;
import org.freenetproject.ForwardPortCallback;
import org.freenetproject.ForwardPortStatus;
/**
* This (and all in org/freenet, org/cybergarage, org/xmlpull)
* grabbed from freenet SVN, mid-February 2009 by zzz.
* This file modded somewhat to remove freenet-specific stuff,
* but most of the glue to I2P is in UPnPManager (which was written
* from scratch and is not the Limewire one referred to below).
*
* ==================
*
* This plugin implements UP&P support on a Freenet node.
*
* @author Florent Daignière <nextgens@freenetproject.org>
*
*
* some code has been borrowed from Limewire : @see com.limegroup.gnutella.UPnPManager
*
* Public only for command line usage. Not a public API, not for external use.
*
* @see "http://www.upnp.org/"
* @see "http://en.wikipedia.org/wiki/Universal_Plug_and_Play"
* @since 0.7.4
*/
/*
* TODO: Support multiple IGDs ?
* TODO: Advertise the node like the MDNS plugin does
* TODO: Implement EventListener and react on ip-change
*/
public class UPnP extends ControlPoint implements DeviceChangeListener, EventListener {
private final Log _log;
private final I2PAppContext _context;
/** 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 WAN_IP_CONNECTION = "urn:schemas-upnp-org:service:WANIPConnection:1";
private static final String WAN_PPP_CONNECTION = "urn:schemas-upnp-org:service:WANPPPConnection:1";
private Device _router;
private Service _service;
// UDN -> friendly name
private final Map<String, String> _otherUDNs;
private boolean isDisabled = false; // We disable the plugin if more than one IGD is found
private volatile boolean _serviceLacksAPM;
private final Object lock = new Object();
// FIXME: detect it for real and deal with it! @see #2524
private volatile boolean thinksWeAreDoubleNatted = false;
/** List of ports we want to forward */
private final Set<ForwardPort> portsToForward;
/** List of ports we have actually forwarded */
private final Set<ForwardPort> portsForwarded;
/** Callback to call when a forward fails or succeeds */
private ForwardPortCallback forwardCallback;
private static final String PROP_ADVANCED = "routerconsole.advanced";
private static final String PROP_IGNORE = "i2np.upnp.ignore";
public UPnP(I2PAppContext context) {
super();
_context = context;
_log = _context.logManager().getLog(UPnP.class);
portsToForward = new HashSet<ForwardPort>();
portsForwarded = new HashSet<ForwardPort>();
_otherUDNs = new HashMap<String, String>(4);
addDeviceChangeListener(this);
}
public synchronized boolean runPlugin() {
synchronized(lock) {
portsToForward.clear();
portsForwarded.clear();
}
return super.start();
}
/**
* WARNING - Blocking up to 2 seconds
*/
public synchronized void terminate() {
synchronized(lock) {
portsToForward.clear();
}
// this gets spun off in a thread...
unregisterPortMappings();
// If we stop too early and we've forwarded multiple ports,
// the later ones don't get unregistered
int i = 0;
while (i++ < 20 && !portsForwarded.isEmpty()) {
try {
Thread.sleep(100);
} catch (InterruptedException ie) {}
}
super.stop();
synchronized(lock) {
_router = null;
_service = null;
_serviceLacksAPM = false;
}
}
public DetectedIP[] getAddress() {
_log.info("UP&P.getAddress() is called \\o/");
if(isDisabled) {
if (_log.shouldLog(Log.WARN))
_log.warn("Plugin has been disabled previously, ignoring request.");
return null;
} else if(!isNATPresent()) {
if (_log.shouldLog(Log.WARN))
_log.warn("No UP&P device found, detection of the external ip address using the plugin has failed");
return null;
}
DetectedIP result = null;
final String natAddress = getNATAddress();
if (natAddress == null || natAddress.length() <= 0) {
if (_log.shouldLog(Log.WARN))
_log.warn("No external address returned");
return null;
}
try {
InetAddress detectedIP = InetAddress.getByName(natAddress);
short status = DetectedIP.NOT_SUPPORTED;
thinksWeAreDoubleNatted = !TransportUtil.isPubliclyRoutable(detectedIP.getAddress(), false);
// If we have forwarded a port AND we don't have a private address
if (_log.shouldLog(Log.WARN))
_log.warn("NATAddress: \"" + natAddress + "\" detectedIP: " + detectedIP + " double? " + thinksWeAreDoubleNatted);
if((portsForwarded.size() > 1) && (!thinksWeAreDoubleNatted))
status = DetectedIP.FULL_INTERNET;
result = new DetectedIP(detectedIP, status);
if (_log.shouldLog(Log.WARN))
_log.warn("Successful UP&P discovery :" + result);
return new DetectedIP[] { result };
} catch (UnknownHostException e) {
_log.error("Caught an UnknownHostException resolving " + natAddress, e);
return null;
}
}
/**
* DeviceChangeListener
*/
public void deviceAdded(Device dev) {
String udn = dev.getUDN();
if (udn == null)
udn = "???";
String name = dev.getFriendlyName();
if (name == null)
name = "???";
boolean isIGD = ROUTER_DEVICE.equals(dev.getDeviceType()) && dev.isRootDevice();
name += isIGD ? " IGD" : (" " + dev.getDeviceType());
synchronized (lock) {
if(isDisabled) {
if (_log.shouldLog(Log.WARN))
_log.warn("Plugin has been disabled previously, ignoring " + name + " UDN: " + udn);
_otherUDNs.put(udn, name);
return;
}
}
if(!ROUTER_DEVICE.equals(dev.getDeviceType()) || !dev.isRootDevice()) {
if (_log.shouldLog(Log.WARN))
_log.warn("UP&P non-IGD device found, ignoring " + name + ' ' + dev.getDeviceType());
synchronized (lock) {
_otherUDNs.put(udn, name);
}
return; // ignore non-IGD devices
} else if(isNATPresent()) {
// maybe we should see if the old one went away before ignoring the new one?
// TODO if old one doesn't have an IP address but new one does, switch
_log.logAlways(Log.WARN, "UP&P ignoring additional device " + name + " UDN: " + udn);
synchronized (lock) {
_otherUDNs.put(udn, name);
}
return;
}
boolean ignore = false;
String toIgnore = _context.getProperty(PROP_IGNORE);
if (toIgnore != null) {
String[] ignores = DataHelper.split(toIgnore, "[,; \r\n\t]");
for (int i = 0; i < ignores.length; i++) {
if (ignores[i].equals(udn)) {
ignore = true;
_log.logAlways(Log.WARN, "Ignoring by config: " + name + " UDN: " + udn);
break;
}
}
}
synchronized(lock) {
if (ignore) {
_otherUDNs.put(udn, name);
return;
} else {
_router = dev;
}
}
if (_log.shouldLog(Log.WARN))
_log.warn("UP&P IGD found : " + name + " UDN: " + udn + " lease time: " + dev.getLeaseTime());
discoverService();
// We have found the device we need: stop the listener thread
/// No, let's stick around to get notifications
//stop();
synchronized(lock) {
/// we should look for the next one
if(_service == null) {
_log.error("The IGD device we got isn't suiting our needs, let's disable the plugin");
//isDisabled = true;
_router = null;
return;
}
subscribe(_service);
}
registerPortMappings();
}
private void registerPortMappings() {
Set<ForwardPort> ports;
synchronized(lock) {
ports = new HashSet<ForwardPort>(portsForwarded);
}
if (ports.isEmpty())
return;
registerPorts(ports);
}
/**
* Traverses the structure of the router device looking for the port mapping service.
*/
private void discoverService() {
synchronized (lock) {
for (Device current : _router.getDeviceList()) {
if (!current.getDeviceType().equals(WAN_DEVICE))
continue;
DeviceList l = current.getDeviceList();
for (int i=0;i<current.getDeviceList().size();i++) {
Device current2 = l.getDevice(i);
if (!current2.getDeviceType().equals(WANCON_DEVICE))
continue;
_service = current2.getService(WAN_PPP_CONNECTION);
if(_service == null) {
if (_log.shouldLog(Log.INFO))
_log.info(_router.getFriendlyName()+ " doesn't seems to be using PPP; we won't be able to extract bandwidth-related informations out of it.");
_service = current2.getService(WAN_IP_CONNECTION);
if(_service == null)
_log.error(_router.getFriendlyName()+ " doesn't export WAN_IP_CONNECTION either: we won't be able to use it!");
}
_serviceLacksAPM = false;
return;
}
}
}
}
private boolean tryAddMapping(String protocol, int port, String description, ForwardPort fp) {
if (_log.shouldLog(Log.WARN))
_log.warn("Registering a port mapping for " + port + "/" + protocol);
int nbOfTries = 0;
boolean isPortForwarded = false;
while ((!_serviceLacksAPM) && nbOfTries++ < 5) {
//isPortForwarded = addMapping(protocol, port, "I2P " + description, fp);
isPortForwarded = addMapping(protocol, port, description, fp);
if(isPortForwarded || _serviceLacksAPM)
break;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {}
}
if (_log.shouldLog(Log.WARN))
_log.warn((isPortForwarded ? "Mapping is successful!" : "Mapping has failed!") + " ("+ nbOfTries + " tries)");
return isPortForwarded;
}
public void unregisterPortMappings() {
Set<ForwardPort> ports;
synchronized(lock) {
ports = new HashSet<ForwardPort>(portsForwarded);
}
if (ports.isEmpty())
return;
this.unregisterPorts(ports);
}
/**
* DeviceChangeListener
*/
public void deviceRemoved(Device dev ){
String udn = dev.getUDN();
if (_log.shouldLog(Log.WARN))
_log.warn("UP&P device removed : " + dev.getFriendlyName() + " UDN: " + udn);
ForwardPortCallback fpc = null;
Map<ForwardPort, ForwardPortStatus> removeMap = null;
synchronized (lock) {
if (udn != null)
_otherUDNs.remove(udn);
else
_otherUDNs.remove("???");
if (_router == null) return;
// I2P this wasn't working
//if(_router.equals(dev)) {
if(ROUTER_DEVICE.equals(dev.getDeviceType()) &&
dev.isRootDevice() &&
stringEquals(_router.getFriendlyName(), dev.getFriendlyName()) &&
stringEquals(_router.getUDN(), udn)) {
if (_log.shouldLog(Log.WARN))
_log.warn("UP&P IGD device removed : " + dev.getFriendlyName());
// TODO promote an IGD from _otherUDNs ??
// For now, just clear the others so they can be promoted later
// after a rescan.
_otherUDNs.clear();
_router = null;
_service = null;
_serviceLacksAPM = false;
if (!portsForwarded.isEmpty()) {
fpc = forwardCallback;
removeMap = new HashMap<ForwardPort, ForwardPortStatus>(portsForwarded.size());
for (ForwardPort port : portsForwarded) {
ForwardPortStatus fps = new ForwardPortStatus(ForwardPortStatus.DEFINITE_FAILURE,
"UPnP device removed",
port.portNumber);
}
}
portsForwarded.clear();
}
}
if (fpc != null) {
fpc.portForwardStatus(removeMap);
}
}
/**
* EventListener callback -
* unused for now - how many devices support events?
*/
public void eventNotifyReceived(String uuid, long seq, String varName, String value) {
if (_log.shouldLog(Log.WARN))
_log.warn("Event: " + uuid + ' ' + seq + ' ' + varName + '=' + value);
}
/** compare two strings, either of which could be null */
private static boolean stringEquals(String a, String b) {
if (a != null)
return a.equals(b);
return b == null;
}
/**
* @return whether we are behind an UPnP-enabled NAT/router
*/
private boolean isNATPresent() {
synchronized(lock) {
return _router != null && _service != null;
}
}
/**
* @return the external address the NAT thinks we have. Blocking.
* null if we can't find it.
*/
private String getNATAddress() {
Service service;
synchronized(lock) {
if(!isNATPresent())
return null;
service = _service;
}
Action getIP = service.getAction("GetExternalIPAddress");
if(getIP == null || !getIP.postControlAction())
return null;
Argument a = getIP.getOutputArgumentList().getArgument("NewExternalIPAddress");
if (a == null)
return null;
String rv = a.getValue();
// I2P some devices return 0.0.0.0 when not connected
if ("0.0.0.0".equals(rv) || rv == null || rv.length() <= 0)
return null;
return rv;
}
/**
* @return the reported upstream bit rate in bits per second. -1 if it's not available. Blocking.
*/
private int getUpstreamMaxBitRate() {
Service service;
synchronized(lock) {
if(!isNATPresent() || thinksWeAreDoubleNatted)
return -1;
service = _service;
}
Action getIP = service.getAction("GetLinkLayerMaxBitRates");
if(getIP == null || !getIP.postControlAction())
return -1;
Argument a = getIP.getOutputArgumentList().getArgument("NewUpstreamMaxBitRate");
if (a == null)
return -1;
try {
return Integer.parseInt(a.getValue());
} catch (NumberFormatException nfe) {
return -1;
}
}
/**
* @return the reported downstream bit rate in bits per second. -1 if it's not available. Blocking.
*/
private int getDownstreamMaxBitRate() {
Service service;
synchronized(lock) {
if(!isNATPresent() || thinksWeAreDoubleNatted)
return -1;
service = _service;
}
Action getIP = service.getAction("GetLinkLayerMaxBitRates");
if(getIP == null || !getIP.postControlAction())
return -1;
Argument a = getIP.getOutputArgumentList().getArgument("NewDownstreamMaxBitRate");
if (a == null)
return -1;
try {
return Integer.parseInt(a.getValue());
} catch (NumberFormatException nfe) {
return -1;
}
}
/** debug only */
private static void listStateTable(Service serv, StringBuilder sb) {
ServiceStateTable table;
try {
table = serv.getServiceStateTable();
} catch (RuntimeException e) {
// getSCPDNode() returns null,
// NPE at org.cybergarage.upnp.Service.getServiceStateTable(Service.java:526)
sb.append(" : no state");
return;
}
sb.append("<ul><small>");
for(int i=0; i<table.size(); i++) {
StateVariable current = table.getStateVariable(i);
sb.append("<li>").append(DataHelper.escapeHTML(current.getName()))
.append(" : \"").append(DataHelper.escapeHTML(current.getValue()))
.append("\"</li>");
}
sb.append("</small></ul>");
}
/** debug only */
private static void listActionsArguments(Action action, StringBuilder sb) {
ArgumentList ar = action.getArgumentList();
sb.append("<ol>");
for(int i=0; i<ar.size(); i++) {
Argument argument = ar.getArgument(i);
if(argument == null ) continue;
sb.append("<li><small>argument : ").append(DataHelper.escapeHTML(argument.getName()))
.append("</small></li>");
}
sb.append("</ol>");
}
/** debug only */
private static void listActions(Service service, StringBuilder sb) {
ActionList al = service.getActionList();
sb.append("<ul>");
for(int i=0; i<al.size(); i++) {
Action action = al.getAction(i);
if(action == null ) continue;
sb.append("<li>").append(DataHelper.escapeHTML(action.getName()));
listActionsArguments(action, sb);
sb.append("</li>");
}
sb.append("</ul>");
}
/**
* A blocking toString(). That's interesting.
* Cache the last ArgumentList to speed it up some.
* Count on listSubServices() to call multiple combinations of arguments
* so we don't get old data.
*/
private String _lastAction;
private Service _lastService;
private ArgumentList _lastArgumentList;
private final Object toStringLock = new Object();
private String toString(String action, String arg, Service serv) {
synchronized(toStringLock) {
if ((!action.equals(_lastAction)) ||
(!serv.equals(_lastService)) ||
_lastArgumentList == null) {
Action getIP = serv.getAction(action);
if(getIP == null || !getIP.postControlAction()) {
_lastAction = null;
return null;
}
_lastAction = action;
_lastService = serv;
_lastArgumentList = getIP.getOutputArgumentList();
}
Argument a = _lastArgumentList.getArgument(arg);
if (a == null)
return "";
String rv = a.getValue();
return DataHelper.escapeHTML(rv);
}
}
// TODO: extend it! RTFM
private void listSubServices(Device dev, StringBuilder sb) {
ServiceList sl = dev.getServiceList();
if (sl.isEmpty())
return;
sb.append("<ul>\n");
for(int i=0; i<sl.size(); i++) {
Service serv = sl.getService(i);
if(serv == null) continue;
sb.append("<li>").append(_t("Service")).append(": ");
// NOTE: Group all toString() of common actions together
// to avoid excess fetches, since toString() caches.
if("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1".equals(serv.getServiceType())){
sb.append(_t("WAN Common Interface Configuration"));
sb.append("<ul><li>").append(_t("Status")).append(": ")
.append(toString("GetCommonLinkProperties", "NewPhysicalLinkStatus", serv));
sb.append("<li>").append(_t("Type")).append(": ")
.append(toString("GetCommonLinkProperties", "NewWANAccessType", serv));
sb.append("<li>").append(_t("Upstream")).append(": ")
.append(toString("GetCommonLinkProperties", "NewLayer1UpstreamMaxBitRate", serv));
sb.append("<li>").append(_t("Downstream")).append(": ")
.append(toString("GetCommonLinkProperties", "NewLayer1DownstreamMaxBitRate", serv))
.append("</li>");
}else if("urn:schemas-upnp-org:service:WANPPPConnection:1".equals(serv.getServiceType())){
sb.append(_t("WAN PPP Connection"));
sb.append("<ul><li>").append(_t("Status")).append(": ")
.append(toString("GetStatusInfo", "NewConnectionStatus", serv));
String up = toString("GetStatusInfo", "NewUptime", serv);
if (up != null) {
try {
long uptime = Long.parseLong(up);
uptime *= 1000;
sb.append("<li>").append(_t("Uptime")).append(": ")
.append(DataHelper.formatDuration2(uptime));
} catch (NumberFormatException nfe) {}
}
sb.append("<li>").append(_t("Type")).append(": ")
.append(toString("GetConnectionTypeInfo", "NewConnectionType", serv));
sb.append("<li>").append(_t("Upstream")).append(": ")
.append(toString("GetLinkLayerMaxBitRates", "NewUpstreamMaxBitRate", serv));
sb.append("<li>").append(_t("Downstream")).append(": ")
.append(toString("GetLinkLayerMaxBitRates", "NewDownstreamMaxBitRate", serv) + "<br>");
sb.append("<li>").append(_t("External IP")).append(": ")
.append(toString("GetExternalIPAddress", "NewExternalIPAddress", serv))
.append("</li>");
}else if("urn:schemas-upnp-org:service:Layer3Forwarding:1".equals(serv.getServiceType())){
sb.append(_t("Layer 3 Forwarding"));
sb.append("<ul><li>").append(_t("Default Connection Service")).append(": ")
.append(toString("GetDefaultConnectionService", "NewDefaultConnectionService", serv))
.append("</li>");
}else if(WAN_IP_CONNECTION.equals(serv.getServiceType())){
sb.append(_t("WAN IP Connection"));
sb.append("<ul><li>").append(_t("Status")).append(": ")
.append(toString("GetStatusInfo", "NewConnectionStatus", serv));
String up = toString("GetStatusInfo", "NewUptime", serv);
if (up != null) {
try {
long uptime = Long.parseLong(up);
uptime *= 1000;
sb.append("<li>").append(_t("Uptime")).append(": ")
.append(DataHelper.formatDuration2(uptime));
} catch (NumberFormatException nfe) {}
}
sb.append("<li>").append(_t("Type")).append(": ")
.append(toString("GetConnectionTypeInfo", "NewConnectionType", serv));
sb.append("<li>").append(_t("External IP")).append(": ")
.append(toString("GetExternalIPAddress", "NewExternalIPAddress", serv))
.append("</li>");
}else if("urn:schemas-upnp-org:service:WANEthernetLinkConfig:1".equals(serv.getServiceType())){
sb.append(_t("WAN Ethernet Link Configuration"));
sb.append("<ul><li>").append(_t("Status")).append(": ")
.append(toString("GetEthernetLinkStatus", "NewEthernetLinkStatus", serv))
.append("</li>");
} else {
sb.append(DataHelper.escapeHTML(serv.getServiceType())).append("<ul>");
}
if (_context.getBooleanProperty(PROP_ADVANCED)) {
sb.append("<li>Actions");
listActions(serv, sb);
sb.append("</li><li>States");
listStateTable(serv, sb);
sb.append("</li>");
}
sb.append("</ul>\n");
}
sb.append("</ul>\n");
}
private void listSubDev(String prefix, Device dev, StringBuilder sb){
if (prefix == null)
sb.append("<p>").append(_t("Found Device")).append(": ");
else
sb.append("<li>").append(_t("Subdevice")).append(": ");
sb.append(DataHelper.escapeHTML(dev.getFriendlyName()));
if (prefix == null)
sb.append("</p>");
listSubServices(dev, sb);
DeviceList dl = dev.getDeviceList();
if (dl.isEmpty())
return;
sb.append("<ul>\n");
for(int j=0; j<dl.size(); j++) {
Device subDev = dl.getDevice(j);
if(subDev == null) continue;
listSubDev(dev.getFriendlyName(), subDev, sb);
}
sb.append("</ul>\n");
}
/** warning - slow */
public String renderStatusHTML() {
final StringBuilder sb = new StringBuilder();
sb.append("<h3><a name=\"upnp\"></a>").append(_t("UPnP Status")).append("</h3>");
synchronized(_otherUDNs) {
if (!_otherUDNs.isEmpty()) {
sb.append(_t("Disabled UPnP Devices"));
sb.append("<ul>");
for (Map.Entry<String, String> e : _otherUDNs.entrySet()) {
String udn = e.getKey();
String name = e.getValue();
sb.append("<li>").append(DataHelper.escapeHTML(name));
sb.append("<br>UDN: ").append(DataHelper.escapeHTML(udn))
.append("</li>");
}
sb.append("</ul>");
}
}
if(isDisabled) {
sb.append(_t("UPnP has been disabled; Do you have more than one UPnP Internet Gateway Device on your LAN ?"));
return sb.toString();
} else if(!isNATPresent()) {
sb.append(_t("UPnP has not found any UPnP-aware, compatible device on your LAN."));
return sb.toString();
}
listSubDev(null, _router, sb);
String addr = getNATAddress();
sb.append("<p>");
if (addr != null)
sb.append(_t("The current external IP address reported by UPnP is {0}", DataHelper.escapeHTML(addr)));
else
sb.append(_t("The current external IP address is not available."));
int downstreamMaxBitRate = getDownstreamMaxBitRate();
int upstreamMaxBitRate = getUpstreamMaxBitRate();
if(downstreamMaxBitRate > 0)
sb.append("<br>").append(_t("UPnP reports the maximum downstream bit rate is {0}bits/sec", DataHelper.formatSize2(downstreamMaxBitRate)));
if(upstreamMaxBitRate > 0)
sb.append("<br>").append(_t("UPnP reports the maximum upstream bit rate is {0}bits/sec", DataHelper.formatSize2(upstreamMaxBitRate)));
synchronized(lock) {
for(ForwardPort port : portsToForward) {
sb.append("<br>");
if(portsForwarded.contains(port))
// {0} is TCP or UDP
// {1,number,#####} prevents 12345 from being output as 12,345 in the English locale.
// If you want the digit separator in your locale, translate as {1}.
sb.append(_t("{0} port {1,number,#####} was successfully forwarded by UPnP.", protoToString(port.protocol), port.portNumber));
else
sb.append(_t("{0} port {1,number,#####} was not forwarded by UPnP.", protoToString(port.protocol), port.portNumber));
}
}
sb.append("</p>");
return sb.toString();
}
/**
* This always requests that the external port == the internal port, for now.
* Blocking!
*/
private boolean addMapping(String protocol, int port, String description, ForwardPort fp) {
Service service;
synchronized(lock) {
if(isDisabled || !isNATPresent() || _router == null) {
_log.error("Can't addMapping: " + isDisabled + " " + isNATPresent() + " " + _router);
return false;
}
service = _service;
}
// Just in case...
// this confuses my linksys? - zzz
//removeMapping(protocol, port, fp, true);
Action add = service.getAction("AddPortMapping");
if(add == null) {
if (_serviceLacksAPM) {
if (_log.shouldLog(Log.WARN))
_log.warn("Couldn't find AddPortMapping action!");
} else {
_serviceLacksAPM = true;
_log.logAlways(Log.WARN, "UPnP device does not support port forwarding");
}
return false;
}
add.setArgumentValue("NewRemoteHost", "");
add.setArgumentValue("NewExternalPort", port);
// bugfix, see below for details
String intf = _router.getInterfaceAddress();
String us = getOurAddress(intf);
if (_log.shouldLog(Log.WARN) && !us.equals(intf))
_log.warn("Requesting port forward to " + us + ':' + port +
" when cybergarage wanted " + intf);
add.setArgumentValue("NewInternalClient", us);
add.setArgumentValue("NewInternalPort", port);
add.setArgumentValue("NewProtocol", protocol);
add.setArgumentValue("NewPortMappingDescription", description);
add.setArgumentValue("NewEnabled","1");
add.setArgumentValue("NewLeaseDuration", 0);
boolean rv = add.postControlAction();
if(rv) {
synchronized(lock) {
portsForwarded.add(fp);
}
}
int level = rv ? Log.INFO : Log.WARN;
if (_log.shouldLog(level)) {
StringBuilder buf = new StringBuilder();
buf.append("AddPortMapping result for ").append(protocol).append(" port ").append(port);
// Not sure which of these has the good info
UPnPStatus status = add.getStatus();
if (status != null)
buf.append(" Status: ").append(status.getCode()).append(' ').append(status.getDescription());
status = add.getControlStatus();
if (status != null)
buf.append(" ControlStatus: ").append(status.getCode()).append(' ').append(status.getDescription());
_log.log(level, buf.toString());
}
// TODO if port is busy, retry with wildcard external port ??
// from spec:
// 402 Invalid Args See UPnP Device Architecture section on Control.
// 501 Action Failed See UPnP Device Architecture section on Control.
// 715 WildCardNotPermittedInSrcIP The source IP address cannot be wild-carded
// 716 WildCardNotPermittedInExtPort The external port cannot be wild-carded
// 718 ConflictInMappingEntry The port mapping entry specified conflicts with a mapping assigned previously to another client
// 724 SamePortValuesRequired Internal and External port values must be the same
// 725 OnlyPermanentLeasesSupported The NAT implementation only supports permanent lease times on port mappings
// 726 RemoteHostOnlySupportsWildcard RemoteHost must be a wildcard and cannot be a specific IP address or DNS name
// 727 ExternalPortOnlySupportsWildcard ExternalPort must be a wildcard and cannot be a specific port value
// TODO return error code and description for display
return rv;
}
/**
* Bug fix:
* If the SSDP notify or search response sockets listen on more than one interface,
* cybergarage can get our IP address wrong, and then we send the wrong one
* to the UPnP device, which will reject it if it enforces strict addressing.
*
* For example, if we have interfaces 192.168.1.1 and 192.168.2.1, we could
* get a response from 192.168.1.99 on the 192.168.2.1 interface, but when
* we send something to 192.168.1.99 it will go out the 192.168.1.1 interface
* with a request to forward to 192.168.2.1.
*
* So return the address of ours that is closest to his.
*
* @since 0.8.8
*/
private String getOurAddress(String deflt) {
String rv = deflt;
String hisIP = null;
// see ControlRequest.setRequestHost()
String him = _router.getURLBase();
if (him != null && him.length() > 0) {
try {
URI url = new URI(him);
hisIP = url.getHost();
} catch (URISyntaxException use) {}
}
if (hisIP == null) {
him = _router.getLocation();
if (him != null && him.length() > 0) {
try {
URI url = new URI(him);
hisIP = url.getHost();
} catch (URISyntaxException use) {}
}
}
if (hisIP == null)
return rv;
try {
byte[] hisBytes = InetAddress.getByName(hisIP).getAddress();
if (hisBytes.length != 4)
return deflt;
long hisLong = DataHelper.fromLong(hisBytes, 0, 4);
long distance = Long.MAX_VALUE;
// loop through all our IP addresses, including the default, and
// return the one closest to the router's IP
Set<String> myAddresses = Addresses.getAddresses(true, false); // yes local, no IPv6
myAddresses.add(deflt);
for (String me : myAddresses) {
if (me.startsWith("127.") || me.equals("0.0.0.0"))
continue;
try {
byte[] myBytes = InetAddress.getByName(me).getAddress();
long myLong = DataHelper.fromLong(myBytes, 0, 4);
long newDistance = myLong ^ hisLong;
if (newDistance < distance) {
rv = me;
distance = newDistance;
}
} catch (UnknownHostException uhe) {}
}
} catch (UnknownHostException uhe) {}
return rv;
}
/** blocking */
private boolean removeMapping(String protocol, int port, ForwardPort fp, boolean noLog) {
Service service;
synchronized(lock) {
if(isDisabled || !isNATPresent()) {
_log.error("Can't removeMapping: " + isDisabled + " " + isNATPresent() + " " + _router);
return false;
}
service = _service;
}
Action remove = service.getAction("DeletePortMapping");
if(remove == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Couldn't find DeletePortMapping action!");
return false;
}
// remove.setArgumentValue("NewRemoteHost", "");
remove.setArgumentValue("NewExternalPort", port);
remove.setArgumentValue("NewProtocol", protocol);
boolean retval = remove.postControlAction();
synchronized(lock) {
portsForwarded.remove(fp);
}
if(_log.shouldLog(Log.WARN) && !noLog)
_log.warn("UPnP: Removed mapping for "+fp.name+" "+port+" / "+protocol);
return retval;
}
/**
* Registers a callback when the given ports change.
* non-blocking
* @param ports non-null
* @param cb in UPnPManager
*/
public void onChangePublicPorts(Set<ForwardPort> ports, ForwardPortCallback cb) {
Set<ForwardPort> portsToDumpNow = null;
Set<ForwardPort> portsToForwardNow = null;
if (_log.shouldLog(Log.INFO))
_log.info("UP&P Forwarding "+ports.size()+" ports...", new Exception());
synchronized(lock) {
if(forwardCallback != null && forwardCallback != cb && cb != null) {
_log.error("ForwardPortCallback changed from "+forwardCallback+" to "+cb+" - using new value, but this is very strange!");
}
forwardCallback = cb;
if (portsToForward.isEmpty()) {
portsToForward.addAll(ports);
portsToForwardNow = ports;
portsToDumpNow = null;
} else if(ports.isEmpty()) {
portsToDumpNow = portsToForward;
portsToForward.clear();
portsToForwardNow = null;
} else {
// Some ports to keep, some ports to dump
// Ports in ports but not in portsToForwardNow we must forward
// Ports in portsToForwardNow but not in ports we must dump
for(ForwardPort port: ports) {
//if(portsToForward.contains(port)) {
// If not in portsForwarded, it wasn't successful, try again
if(portsForwarded.contains(port)) {
// We have forwarded it, and it should be forwarded, cool.
// Big problem here, if firewall resets, we don't know it.
// Do we need to re-forward anyway? or poll the router?
} else {
// Needs forwarding
if(portsToForwardNow == null) portsToForwardNow = new HashSet<ForwardPort>();
portsToForwardNow.add(port);
}
}
for(ForwardPort port : portsToForward) {
if(ports.contains(port)) {
// Should be forwarded, has been forwarded, cool.
} else {
// Needs dropping
if(portsToDumpNow == null) portsToDumpNow = new HashSet<ForwardPort>();
portsToDumpNow.add(port);
}
}
portsToForward.clear();
portsToForward.addAll(ports);
}
if(_router == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("No UPnP router available to update");
return; // When one is found, we will do the forwards
}
}
if(portsToDumpNow != null && !portsToDumpNow.isEmpty())
unregisterPorts(portsToDumpNow);
if(portsToForwardNow != null && !portsToForwardNow.isEmpty())
registerPorts(portsToForwardNow);
}
private static String protoToString(int p) {
if(p == ForwardPort.PROTOCOL_UDP_IPV4)
return "UDP";
if(p == ForwardPort.PROTOCOL_TCP_IPV4)
return "TCP";
return "?";
}
private static final AtomicInteger __id = new AtomicInteger();
/**
* postControlAction() can take many seconds, especially if it's failing,
* and onChangePublicPorts() may be called from threads we don't want to slow down,
* so throw this in a thread.
*/
private void registerPorts(Set<ForwardPort> portsToForwardNow) {
if (_serviceLacksAPM) {
if (_log.shouldLog(Log.WARN))
_log.warn("UPnP device does not support port forwarding");
Map<ForwardPort, ForwardPortStatus> map =
new HashMap<ForwardPort, ForwardPortStatus>(portsToForwardNow.size());
for (ForwardPort port : portsToForwardNow) {
ForwardPortStatus fps = new ForwardPortStatus(ForwardPortStatus.DEFINITE_FAILURE,
"UPnP device does not support port forwarding",
port.portNumber);
map.put(port, fps);
}
forwardCallback.portForwardStatus(map);
return;
}
if (_log.shouldLog(Log.INFO))
_log.info("Starting thread to forward " + portsToForwardNow.size() + " ports");
Thread t = new I2PThread(new RegisterPortsThread(portsToForwardNow));
t.setName("UPnP Port Opener " + __id.incrementAndGet());
t.setDaemon(true);
t.start();
}
private class RegisterPortsThread implements Runnable {
private Set<ForwardPort> portsToForwardNow;
public RegisterPortsThread(Set<ForwardPort> ports) {
portsToForwardNow = ports;
}
public void run() {
Map<ForwardPort, ForwardPortStatus> map =
new HashMap<ForwardPort, ForwardPortStatus>(portsToForwardNow.size());
for(ForwardPort port : portsToForwardNow) {
String proto = protoToString(port.protocol);
ForwardPortStatus fps;
if (proto.length() <= 1) {
fps = new ForwardPortStatus(ForwardPortStatus.DEFINITE_FAILURE, "Protocol not supported", port.portNumber);
} else if(tryAddMapping(proto, port.portNumber, port.name, port)) {
fps = new ForwardPortStatus(ForwardPortStatus.MAYBE_SUCCESS, "Port apparently forwarded by UPnP", port.portNumber);
} else {
fps = new ForwardPortStatus(ForwardPortStatus.PROBABLE_FAILURE, "UPnP port forwarding apparently failed", port.portNumber);
}
map.put(port, fps);
}
forwardCallback.portForwardStatus(map);
}
}
/**
* postControlAction() can take many seconds, especially if it's failing,
* and onChangePublicPorts() may be called from threads we don't want to slow down,
* so throw this in a thread.
*/
private void unregisterPorts(Set<ForwardPort> portsToForwardNow) {
if (_log.shouldLog(Log.INFO))
_log.info("Starting thread to un-forward " + portsToForwardNow.size() + " ports");
Thread t = new I2PThread(new UnregisterPortsThread(portsToForwardNow));
t.setName("UPnP Port Closer " + __id.incrementAndGet());
t.setDaemon(true);
t.start();
}
private class UnregisterPortsThread implements Runnable {
private Set<ForwardPort> portsToForwardNow;
public UnregisterPortsThread(Set<ForwardPort> ports) {
portsToForwardNow = ports;
}
public void run() {
for(ForwardPort port : portsToForwardNow) {
String proto = protoToString(port.protocol);
if (proto.length() <= 1)
// Ignore, we've already complained about it
continue;
removeMapping(proto, port.portNumber, port, false);
}
}
}
/**
* Dumps out device info in semi-HTML format
*/
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.setProperty(PROP_ADVANCED, "true");
I2PAppContext ctx = new I2PAppContext(props);
UPnP upnp = new UPnP(ctx);
ControlPoint cp = new ControlPoint();
long start = System.currentTimeMillis();
cp.start();
long s2 = System.currentTimeMillis();
System.out.println("Start took " + (s2 - start));
System.out.println("Searching for UPnP devices");
start = System.currentTimeMillis();
cp.search();
s2 = System.currentTimeMillis();
System.out.println("Search kickoff took " + (s2 - start));
System.out.println("Waiting 10 seconds for responses");
Thread.sleep(10000);
//while(true) {
DeviceList list = cp.getDeviceList();
System.out.println("Found " + list.size() + " devices!");
StringBuilder sb = new StringBuilder();
Iterator<Device> it = list.iterator();
int i = 0;
while(it.hasNext()) {
Device device = it.next();
upnp.listSubDev(device.toString(), device, sb);
System.out.println("Here is the listing for device " + (++i) +
": " + device.getFriendlyName() + " :");
System.out.println(sb.toString());
sb.setLength(0);
}
//}
System.exit(0);
}
private static final String BUNDLE_NAME = "net.i2p.router.web.messages";
/**
* Translate
*/
private final String _t(String s) {
return Translate.getString(s, _context, BUNDLE_NAME);
}
/**
* Translate
*/
private final String _t(String s, Object o) {
return Translate.getString(s, o, _context, BUNDLE_NAME);
}
/**
* Translate
*/
private final String _t(String s, Object o, Object o2) {
return Translate.getString(s, o, o2, _context, BUNDLE_NAME);
}
}