/* 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 plugins.UPnP; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import plugins.UPnP.org.cybergarage.upnp.Action; import plugins.UPnP.org.cybergarage.upnp.ActionList; import plugins.UPnP.org.cybergarage.upnp.Argument; import plugins.UPnP.org.cybergarage.upnp.ArgumentList; import plugins.UPnP.org.cybergarage.upnp.ControlPoint; import plugins.UPnP.org.cybergarage.upnp.Device; import plugins.UPnP.org.cybergarage.upnp.DeviceList; import plugins.UPnP.org.cybergarage.upnp.Service; import plugins.UPnP.org.cybergarage.upnp.ServiceList; import plugins.UPnP.org.cybergarage.upnp.ServiceStateTable; import plugins.UPnP.org.cybergarage.upnp.StateVariable; import plugins.UPnP.org.cybergarage.upnp.device.DeviceChangeListener; import freenet.pluginmanager.DetectedIP; import freenet.pluginmanager.ForwardPort; import freenet.pluginmanager.ForwardPortCallback; import freenet.pluginmanager.ForwardPortStatus; import freenet.pluginmanager.FredPlugin; import freenet.pluginmanager.FredPluginBandwidthIndicator; import freenet.pluginmanager.FredPluginHTTP; import freenet.pluginmanager.FredPluginIPDetector; import freenet.pluginmanager.FredPluginPortForward; import freenet.pluginmanager.FredPluginRealVersioned; import freenet.pluginmanager.FredPluginThreadless; import freenet.pluginmanager.FredPluginVersioned; import freenet.pluginmanager.PluginHTTPException; import freenet.pluginmanager.PluginRespirator; import freenet.support.HTMLNode; import freenet.support.Logger; import freenet.support.api.HTTPRequest; import freenet.support.transport.ip.IPUtil; /** * 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 * * @see http://www.upnp.org/ * @see http://en.wikipedia.org/wiki/Universal_Plug_and_Play * * 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 FredPluginHTTP, FredPlugin, FredPluginThreadless, FredPluginIPDetector, FredPluginPortForward, FredPluginBandwidthIndicator, FredPluginVersioned, FredPluginRealVersioned, DeviceChangeListener { private PluginRespirator pr; /** 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; private boolean isDisabled = false; // We disable the plugin if more than one IGD is found 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 Set portsToForward; /** List of ports we have actually forwarded */ private Set portsForwarded; /** Callback to call when a forward fails or succeeds */ private ForwardPortCallback forwardCallback; public UPnP() { super(); portsForwarded = new HashSet(); addDeviceChangeListener(this); } public void runPlugin(PluginRespirator pr) { this.pr = pr; super.start(); } public void terminate() { unregisterPortMappings(); super.stop(); } public DetectedIP[] getAddress() { Logger.minor(this, "UP&P.getAddress() is called \\o/"); if(isDisabled) { Logger.normal(this, "Plugin has been disabled previously, ignoring request."); return null; } else if(!isNATPresent()) { Logger.normal(this, "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(); try { InetAddress detectedIP = InetAddress.getByName(natAddress); short status = DetectedIP.NOT_SUPPORTED; thinksWeAreDoubleNatted = !IPUtil.isValidAddress(detectedIP, false); // If we have forwarded a port AND we don't have a private address if((portsForwarded.size() > 1) && (!thinksWeAreDoubleNatted)) status = DetectedIP.FULL_INTERNET; result = new DetectedIP(detectedIP, status); Logger.normal(this, "Successful UP&P discovery :" + result); System.out.println("Successful UP&P discovery :" + result); return new DetectedIP[] { result }; } catch (UnknownHostException e) { Logger.error(this, "Caught an UnknownHostException resolving " + natAddress, e); System.err.println("UP&P discovery has failed: unable to resolve " + result); return null; } } public void deviceAdded(Device dev) { synchronized (lock) { if(isDisabled) { Logger.normal(this, "Plugin has been disabled previously, ignoring new device."); return; } } if(!ROUTER_DEVICE.equals(dev.getDeviceType()) || !dev.isRootDevice()) return; // Silently ignore non-IGD devices else if(isNATPresent()) { Logger.error(this, "We got a second IGD on the network! the plugin doesn't handle that: let's disable it."); System.err.println("The UP&P plugin has found more than one IGD on the network, as a result it will be disabled"); isDisabled = true; synchronized(lock) { _router = null; _service = null; } stop(); return; } Logger.normal(this, "UP&P IGD found : " + dev.getFriendlyName()); System.out.println("UP&P IGD found : " + dev.getFriendlyName()); synchronized(lock) { _router = dev; } discoverService(); // We have found the device we need: stop the listener thread stop(); synchronized(lock) { if(_service == null) { Logger.error(this, "The IGD device we got isn't suiting our needs, let's disable the plugin"); System.err.println("The IGD device we got isn't suiting our needs, let's disable the plugin"); isDisabled = true; _router = null; return; } } registerPortMappings(); } private void registerPortMappings() { Set ports; synchronized(lock) { ports = portsToForward; } if(ports == null) return; registerPorts(ports); } /** * Traverses the structure of the router device looking for the port mapping service. */ private void discoverService() { synchronized (lock) { for (Iterator iter = _router.getDeviceList().iterator();iter.hasNext();) { Device current = (Device)iter.next(); if (!current.getDeviceType().equals(WAN_DEVICE)) continue; DeviceList l = current.getDeviceList(); for (int i=0;i"); for(int i=0; i"); } sb.append(""); } private void listActionsArguments(Action action, StringBuilder sb) { ArgumentList ar = action.getArgumentList(); for(int i=0; iargument ("+i+") :" + argument.getName()+""); } } private void listActions(Service service, StringBuilder sb) { ActionList al = service.getActionList(); for(int i=0; iaction ("+i+") :" + action.getName()); listActionsArguments(action, sb); sb.append(""); } } private String toString(String action, String Argument, Service serv) { Action getIP = serv.getAction(action); if(getIP == null || !getIP.postControlAction()) return null; Argument ret = getIP.getOutputArgumentList().getArgument(Argument); return ret.getValue(); } // TODO: extend it! RTFM private void listSubServices(Device dev, StringBuilder sb) { ServiceList sl = dev.getServiceList(); for(int i=0; iservice ("+i+") : "+serv.getServiceType()+"
"); if("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1".equals(serv.getServiceType())){ sb.append("WANCommonInterfaceConfig"); sb.append(" status: " + toString("GetCommonLinkProperties", "NewPhysicalLinkStatus", serv)); sb.append(" type: " + toString("GetCommonLinkProperties", "NewWANAccessType", serv)); sb.append(" upstream: " + toString("GetCommonLinkProperties", "NewLayer1UpstreamMaxBitRate", serv)); sb.append(" downstream: " + toString("GetCommonLinkProperties", "NewLayer1DownstreamMaxBitRate", serv) + "
"); }else if("urn:schemas-upnp-org:service:WANPPPConnection:1".equals(serv.getServiceType())){ sb.append("WANPPPConnection"); sb.append(" status: " + toString("GetStatusInfo", "NewConnectionStatus", serv)); sb.append(" type: " + toString("GetConnectionTypeInfo", "NewConnectionType", serv)); sb.append(" upstream: " + toString("GetLinkLayerMaxBitRates", "NewUpstreamMaxBitRate", serv)); sb.append(" downstream: " + toString("GetLinkLayerMaxBitRates", "NewDownstreamMaxBitRate", serv) + "
"); sb.append(" external IP: " + toString("GetExternalIPAddress", "NewExternalIPAddress", serv) + "
"); }else if("urn:schemas-upnp-org:service:Layer3Forwarding:1".equals(serv.getServiceType())){ sb.append("Layer3Forwarding"); sb.append("DefaultConnectionService: " + toString("GetDefaultConnectionService", "NewDefaultConnectionService", serv)); }else if(WAN_IP_CONNECTION.equals(serv.getServiceType())){ sb.append("WANIPConnection"); sb.append(" status: " + toString("GetStatusInfo", "NewConnectionStatus", serv)); sb.append(" type: " + toString("GetConnectionTypeInfo", "NewConnectionType", serv)); sb.append(" external IP: " + toString("GetExternalIPAddress", "NewExternalIPAddress", serv) + "
"); }else if("urn:schemas-upnp-org:service:WANEthernetLinkConfig:1".equals(serv.getServiceType())){ sb.append("WANEthernetLinkConfig"); sb.append(" status: " + toString("GetEthernetLinkStatus", "NewEthernetLinkStatus", serv) + "
"); }else sb.append("~~~~~~~ "+serv.getServiceType()); listActions(serv, sb); listStateTable(serv, sb); sb.append(""); } } private void listSubDev(String prefix, Device dev, StringBuilder sb){ sb.append("

Device : "+dev.getFriendlyName()+" - "+ dev.getDeviceType()+"
"); listSubServices(dev, sb); DeviceList dl = dev.getDeviceList(); for(int j=0; j"); listSubDev(dev.getFriendlyName(), subDev, sb); sb.append("

"); } sb.append("

"); } public String handleHTTPGet(HTTPRequest request) throws PluginHTTPException { if(request.isParameterSet("getDeviceCapabilities")) { final StringBuilder sb = new StringBuilder(); sb.append("UPnP report"); listSubDev("WANDevice", _router, sb); sb.append(""); return sb.toString(); } HTMLNode pageNode = pr.getPageMaker().getPageNode("UP&P plugin configuration page", false, null); HTMLNode contentNode = pr.getPageMaker().getContentNode(pageNode); if(isDisabled) { HTMLNode disabledInfobox = contentNode.addChild("div", "class", "infobox infobox-error"); HTMLNode disabledInfoboxHeader = disabledInfobox.addChild("div", "class", "infobox-header"); HTMLNode disabledInfoboxContent = disabledInfobox.addChild("div", "class", "infobox-content"); disabledInfoboxHeader.addChild("#", "UP&P plugin report"); disabledInfoboxContent.addChild("#", "The plugin has been disabled; Do you have more than one UP&P IGD on your LAN ?"); return pageNode.generate(); } else if(!isNATPresent()) { HTMLNode notFoundInfobox = contentNode.addChild("div", "class", "infobox infobox-warning"); HTMLNode notFoundInfoboxHeader = notFoundInfobox.addChild("div", "class", "infobox-header"); HTMLNode notFoundInfoboxContent = notFoundInfobox.addChild("div", "class", "infobox-content"); notFoundInfoboxHeader.addChild("#", "UP&P plugin report"); notFoundInfoboxContent.addChild("#", "The plugin hasn't found any UP&P aware, compatible device on your LAN."); return pageNode.generate(); } HTMLNode foundInfobox = contentNode.addChild("div", "class", "infobox infobox-normal"); HTMLNode foundInfoboxHeader = foundInfobox.addChild("div", "class", "infobox-header"); HTMLNode foundInfoboxContent = foundInfobox.addChild("div", "class", "infobox-content"); // FIXME L10n! foundInfoboxHeader.addChild("#", "UP&P plugin report"); foundInfoboxContent.addChild("p", "The following device has been found : ").addChild("a", "href", "?getDeviceCapabilities").addChild("#", _router.getFriendlyName()); foundInfoboxContent.addChild("p", "Our current external ip address is : " + getNATAddress()); int downstreamMaxBitRate = getDownstreamMaxBitRate(); int upstreamMaxBitRate = getUpstramMaxBitRate(); if(downstreamMaxBitRate > 0) foundInfoboxContent.addChild("p", "Our reported max downstream bit rate is : " + getDownstreamMaxBitRate()+ " bits/sec"); if(upstreamMaxBitRate > 0) foundInfoboxContent.addChild("p", "Our reported max upstream bit rate is : " + getUpstramMaxBitRate()+ " bits/sec"); synchronized(lock) { if(portsToForward != null) { for(ForwardPort port : portsToForward) { if(portsForwarded.contains(port)) { foundInfoboxContent.addChild("p", "The "+port.name+" port "+port.portNumber+" / "+port.protocol+" has been forwarded successfully."); } else { foundInfoboxContent.addChild("p", "The "+port.name+" port "+port.portNumber+" / "+port.protocol+" has not been forwarded."); } } } } return pageNode.generate(); } public String handleHTTPPost(HTTPRequest request) throws PluginHTTPException { return null; } private boolean addMapping(String protocol, int port, String description, ForwardPort fp) { if(isDisabled || !isNATPresent() || _router == null) return false; // Just in case... removeMapping(protocol, port, fp, true); Action add = _service.getAction("AddPortMapping"); if(add == null) { Logger.error(this, "Couldn't find AddPortMapping action!"); return false; } add.setArgumentValue("NewRemoteHost", ""); add.setArgumentValue("NewExternalPort", port); add.setArgumentValue("NewInternalClient", _router.getInterfaceAddress()); add.setArgumentValue("NewInternalPort", port); add.setArgumentValue("NewProtocol", protocol); add.setArgumentValue("NewPortMappingDescription", description); add.setArgumentValue("NewEnabled","1"); add.setArgumentValue("NewLeaseDuration", 0); if(add.postControlAction()) { synchronized(lock) { portsForwarded.add(fp); } return true; } else return false; } private boolean removeMapping(String protocol, int port, ForwardPort fp, boolean noLog) { if(isDisabled || !isNATPresent()) return false; Action remove = _service.getAction("DeletePortMapping"); if(remove == null) { Logger.error(this, "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(!noLog) System.err.println("UPnP: Removed mapping for "+fp.name+" "+port+" / "+protocol); return retval; } public void onChangePublicPorts(Set ports, ForwardPortCallback cb) { Set portsToDumpNow = null; Set portsToForwardNow = null; System.err.println("UP&P Forwarding "+ports.size()+" ports..."); synchronized(lock) { if(forwardCallback != null && forwardCallback != cb && cb != null) { Logger.error(this, "ForwardPortCallback changed from "+forwardCallback+" to "+cb+" - using new value, but this is very strange!"); } forwardCallback = cb; if(portsToForward == null || portsToForward.isEmpty()) { portsToForward = ports; portsToForwardNow = ports; portsToDumpNow = null; } else if(ports == null || ports.isEmpty()) { portsToDumpNow = portsToForward; portsToForward = ports; 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)) { // We have forwarded it, and it should be forwarded, cool. } else { // Needs forwarding if(portsToForwardNow == null) portsToForwardNow = new HashSet(); 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(); portsToDumpNow.add(port); } } portsToForward = ports; } if(_router == null) return; // When one is found, we will do the forwards } if(portsToDumpNow != null) unregisterPorts(portsToDumpNow); if(portsToForwardNow != null) registerPorts(portsToForwardNow); } private void registerPorts(Set portsToForwardNow) { for(ForwardPort port : portsToForwardNow) { String proto; if(port.protocol == ForwardPort.PROTOCOL_UDP_IPV4) proto = "UDP"; else if(port.protocol == ForwardPort.PROTOCOL_TCP_IPV4) proto = "TCP"; else { HashMap map = new HashMap(); map.put(port, new ForwardPortStatus(ForwardPortStatus.DEFINITE_FAILURE, "Protocol not supported", port.portNumber)); forwardCallback.portForwardStatus(map); continue; } if(tryAddMapping(proto, port.portNumber, port.name, port)) { HashMap map = new HashMap(); map.put(port, new ForwardPortStatus(ForwardPortStatus.MAYBE_SUCCESS, "Port apparently forwarded by UPnP", port.portNumber)); forwardCallback.portForwardStatus(map); continue; } else { HashMap map = new HashMap(); map.put(port, new ForwardPortStatus(ForwardPortStatus.PROBABLE_FAILURE, "UPnP port forwarding apparently failed", port.portNumber)); forwardCallback.portForwardStatus(map); continue; } } } private void unregisterPorts(Set portsToForwardNow) { for(ForwardPort port : portsToForwardNow) { String proto; if(port.protocol == ForwardPort.PROTOCOL_UDP_IPV4) proto = "UDP"; else if(port.protocol == ForwardPort.PROTOCOL_TCP_IPV4) proto = "TCP"; else { // Ignore, we've already complained about it continue; } removeMapping(proto, port.portNumber, port, false); } } public String getVersion() { return Version.getVersion() + " " + Version.getSvnRevision(); } public long getRealVersion() { return Version.getRealVersion(); } public static void main(String[] args) throws Exception { UPnP upnp = new UPnP(); ControlPoint cp = new ControlPoint(); System.out.println("Searching for up&p devices:"); cp.start(); cp.search(); while(true) { DeviceList list = cp.getDeviceList(); System.out.println("Found " + list.size() + " devices!"); StringBuilder sb = new StringBuilder(); Iterator it = list.iterator(); while(it.hasNext()) { Device device = it.next(); upnp.listSubDev(device.toString(), device, sb); System.out.println("Here is the listing for " + device.toString() + " :"); System.out.println(sb.toString()); sb = new StringBuilder(); } System.out.println("End"); Thread.sleep(2000); } } }