diff --git a/src/main/java/net/rptools/maptool/util/UPnPUtil.java b/src/main/java/net/rptools/maptool/util/UPnPUtil.java index 34221c310e..a5e982b3c2 100644 --- a/src/main/java/net/rptools/maptool/util/UPnPUtil.java +++ b/src/main/java/net/rptools/maptool/util/UPnPUtil.java @@ -14,195 +14,110 @@ */ package net.rptools.maptool.util; -import java.io.IOException; -import java.net.Inet4Address; -import java.net.InterfaceAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import net.rptools.maptool.client.AppPreferences; import net.rptools.maptool.client.MapTool; -import net.sbbi.upnp.Discovery; -import net.sbbi.upnp.impls.InternetGatewayDevice; -import net.sbbi.upnp.messages.ActionResponse; -import net.sbbi.upnp.messages.UPNPResponseException; +import net.rptools.maptool.util.upnp.PortMappingListener; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jupnp.DefaultUpnpServiceConfiguration; +import org.jupnp.UpnpService; +import org.jupnp.UpnpServiceImpl; +import org.jupnp.model.action.ActionInvocation; +import org.jupnp.model.message.header.STAllHeader; +import org.jupnp.model.meta.Service; +import org.jupnp.support.model.PortMapping; /** * @author Phil Wright + * @author Richard Maw - Rewritten to use jupnp */ public class UPnPUtil { private static final Logger log = LogManager.getLogger(UPnPUtil.class); - private static Map igds; - private static List mappings; - public static boolean findIGDs() { - igds = new HashMap(); - try { - Enumeration e = NetworkInterface.getNetworkInterfaces(); - while (e.hasMoreElements()) { - NetworkInterface ni = e.nextElement(); - try { - var addresses = Collections.list(ni.getInetAddresses()); - if (addresses.isEmpty()) { - log.info("UPnP: Rejecting interface '{}' as it has no addresses", ni.getDisplayName()); - } else if (ni.isLoopback()) { - log.info( - "UPnP: Rejecting interface '{}' [{}] as it is a loopback", - ni.getDisplayName(), - addresses); - } else if (ni.isVirtual()) { - log.info( - "UPnP: Rejecting interface '{}' [{}] as it is virtual", - ni.getDisplayName(), - addresses); - } else if (!ni.isUp()) { - log.info( - "UPnP: Rejecting interface '{}' [{}] as it is not up", - ni.getDisplayName(), - addresses); - } else { - int found = 0; - try { - log.info( - "UPnP: Looking for gateway devices on interface '{}' [{}]", - ni.getDisplayName(), - addresses); - InternetGatewayDevice[] thisNI; - thisNI = - InternetGatewayDevice.getDevices( - AppPreferences.upnpDiscoveryTimeout.get(), - Discovery.DEFAULT_TTL, - Discovery.DEFAULT_MX, - ni); - if (thisNI != null) { - for (InternetGatewayDevice igd : thisNI) { - found++; - log.info("UPnP: Found IGD: {}", igd.getIGDRootDevice().getModelName()); - if (igds.put(igd, ni) != null) { - // There was a previous mapping for this IGD! It's unlikely to have two NICs on - // the - // the same network segment, but it IS possible. For example, both a wired and - // wireless connection using the same router as the gateway. For our purposes it - // doesn't really matter which one we use, but in the future we should give the - // user a choice. - // FIXME We SHOULD be using the "networking binding order" (Windows) - // or "network service order" on OSX. - log.info("UPnP: This was not the first time this IGD was found!"); - } - } - } - } catch (IOException ex) { - // some IO Exception occurred during communication with device - log.warn("While searching for internet gateway devices", ex); - } - log.info("Found {} IGDs on interface {}", found, ni.getDisplayName()); - } - } catch (SocketException se) { - continue; - } - } - } catch (SocketException se) { - // Nothing to do, but we DO want the 'mappings' member to be initialized - } - mappings = new ArrayList(igds.size()); - return !igds.isEmpty(); - } + private record MappingInfo( + UpnpService upnpService, CompletableFuture somePortUnmapped) {} + + private static Map mappingServices = new HashMap(); + /** + * Maps the provided port to a heuristically chosen address for every discovered IGD. + * + * @return true if any port was mapped within the timeout, false if none were discovered or + * weren't mappable within the timeout. + */ public static boolean openPort(int port) { - if (igds == null || igds.isEmpty()) { - findIGDs(); - } - if (igds == null || igds.isEmpty()) { - MapTool.showError("msg.error.server.upnp.noigd"); - return false; - } - for (var entry : igds.entrySet()) { - InternetGatewayDevice gd = entry.getKey(); - NetworkInterface ni = entry.getValue(); - String localHostIP = "(NULL)"; + UpnpService upnpService = new UpnpServiceImpl(new DefaultUpnpServiceConfiguration()); + upnpService.startup(); + + var someDeviceFound = new CompletableFuture(); + var somePortMapped = new CompletableFuture(); + var somePortUnmapped = new CompletableFuture(); + var listener = + new PortMappingListener(new PortMapping(port, null, PortMapping.Protocol.TCP, "MapTool")) { + @Override + public void handleInternetGatewayDeviceFound(Service service) { + super.handleInternetGatewayDeviceFound(service); + someDeviceFound.complete(null); + } + + @Override + protected void handleSuccessfulMapping( + Service service, PortMapping pm, ActionInvocation invocation) { + super.handleSuccessfulMapping(service, pm, invocation); + somePortMapped.complete(null); + } + + @Override + protected void handleSuccessfulUnmapping( + Service service, PortMapping pm, ActionInvocation invocation) { + super.handleSuccessfulUnmapping(service, pm, invocation); + somePortUnmapped.complete(true); + } + }; + upnpService.getRegistry().addListener(listener); + + upnpService.getControlPoint().search(new STAllHeader()); + try { + try { + someDeviceFound.get(AppPreferences.upnpDiscoveryTimeout.get(), TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + MapTool.showError("msg.error.server.upnp.noigd"); + throw e; + } try { - switch (ni.getInterfaceAddresses().size()) { - case 0: - log.error("IGD shows up in list of IGDs, but no NICs stored therein?!"); - break; - case 1: - localHostIP = ni.getInterfaceAddresses().get(0).getAddress().getHostAddress(); - break; - default: - for (InterfaceAddress ifAddr : ni.getInterfaceAddresses()) { - if (ifAddr.getAddress() instanceof Inet4Address) { - localHostIP = ifAddr.getAddress().getHostAddress(); - log.info("IP address {} on interface {}", localHostIP, ni.getDisplayName()); - } - } - break; - } - boolean mapped = gd.addPortMapping("MapTool", null, port, port, localHostIP, 0, "TCP"); - if (mapped) { - mappings.add(gd); - log.info( - "UPnP: Port {} mapped on {} at address {}", port, ni.getDisplayName(), localHostIP); - } - } catch (UPNPResponseException respEx) { - // oops the IGD did not like something !! - log.error( - "UPnP Error 1: Could not add port mapping on device " - + ni.getDisplayName() - + ", IP address " - + localHostIP, - respEx); - } catch (IOException ioe) { - log.error( - "UPnP Error 2: Could not add port mapping on device " - + ni.getDisplayName() - + ", IP address " - + localHostIP, - ioe); + somePortMapped.get(AppPreferences.upnpDiscoveryTimeout.get(), TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + MapTool.showError("UPnP: found some IGDs but no port mapping succeeded!?"); + throw e; } + } catch (ExecutionException | InterruptedException | TimeoutException e) { + upnpService.shutdown(); + return false; } - if (mappings.isEmpty()) - MapTool.showError("UPnP: found " + igds.size() + " IGDs but no port mapping succeeded!?"); - return !mappings.isEmpty(); + mappingServices.put(port, new MappingInfo(upnpService, somePortUnmapped)); + return true; } + /** + * Unmap the provided port from discovered IGDs. + * + * @return true if any mapped ports were successfully unmapped or there were no mappings, false if + * there were mappings that couldn't be unmapped. + */ public static boolean closePort(int port) { - if (igds == null || igds.isEmpty()) return true; - - int count = 0; - for (var iter = igds.entrySet().iterator(); iter.hasNext(); ) { - var entry = iter.next(); - InternetGatewayDevice gd = entry.getKey(); - try { - ActionResponse actResp = gd.getSpecificPortMappingEntry(null, port, "TCP"); - if (actResp != null - && "MapTool".equals(actResp.getOutActionArgumentValue("NewPortMappingDescription"))) { - // NewInternalPort=51234 - // NewEnabled=1 - // NewInternalClient=192.168.0.30 - // NewLeaseDuration=0 - // NewPortMappingDescription=MapTool - boolean unmapped = gd.deletePortMapping(null, port, "TCP"); - if (unmapped) { - count++; - log.info("UPnP: Port unmapped from {}", entry.getValue().getDisplayName()); - iter.remove(); - } else { - log.info("UPnP: Failed to unmap port from {}", entry.getValue().getDisplayName()); - } - } - } catch (IOException e) { - log.info("UPnP: IOException while talking to IGD", e); - } catch (UPNPResponseException e) { - log.info("UPnP: UPNPResponseException while talking to IGD", e); - } + if (!mappingServices.containsKey(port)) { + return true; } - return count > 0; + + var mappingInfo = mappingServices.get(port); + mappingInfo.upnpService().shutdown(); + mappingServices.remove(port); + return mappingInfo.somePortUnmapped().getNow(false); } }