/* * This software copyright by various authors including the RPTools.net * development team, and licensed under the LGPL Version 3 or, at your * option, any later version. * * Portions of this software were originally covered under the Apache * Software License, Version 1.1 or Version 2.0. * * See the file LICENSE elsewhere in this distribution for license details. */ package net.sbbi.upnp.impls; import java.io.IOException; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.UnknownHostException; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import net.sbbi.upnp.Discovery; import net.sbbi.upnp.devices.UPNPDevice; import net.sbbi.upnp.devices.UPNPRootDevice; import net.sbbi.upnp.messages.ActionMessage; import net.sbbi.upnp.messages.ActionResponse; import net.sbbi.upnp.messages.StateVariableMessage; import net.sbbi.upnp.messages.StateVariableResponse; import net.sbbi.upnp.messages.UPNPMessageFactory; import net.sbbi.upnp.messages.UPNPResponseException; import net.sbbi.upnp.services.UPNPService; import org.apache.log4j.Logger; /** * This class can be used to access some funtionalities on the InternetGatewayDevice on your network without having to * know anything about the required input/output parameters. All device functions are not provided. * * @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a> * @version 1.0 */ public class InternetGatewayDevice { private final static Logger log = Logger.getLogger(InternetGatewayDevice.class); private final UPNPRootDevice igd; private UPNPMessageFactory msgFactory; public InternetGatewayDevice(UPNPRootDevice igd) throws UnsupportedOperationException { this(igd, true, true); } private InternetGatewayDevice(UPNPRootDevice igd, boolean WANIPConnection, boolean WANPPPConnection) throws UnsupportedOperationException { this.igd = igd; UPNPDevice myIGDWANConnDevice = igd.getChildDevice("urn:schemas-upnp-org:device:WANConnectionDevice:1"); if (myIGDWANConnDevice == null) { throw new UnsupportedOperationException("device urn:schemas-upnp-org:device:WANConnectionDevice:1 not supported by IGD device " + igd.getModelName()); } UPNPService wanIPSrv = myIGDWANConnDevice.getService("urn:schemas-upnp-org:service:WANIPConnection:1"); UPNPService wanPPPSrv = myIGDWANConnDevice.getService("urn:schemas-upnp-org:service:WANPPPConnection:1"); if ((WANIPConnection && WANPPPConnection) && (wanIPSrv == null && wanPPPSrv == null)) { throw new UnsupportedOperationException("Unable to find any urn:schemas-upnp-org:service:WANIPConnection:1 or urn:schemas-upnp-org:service:WANPPPConnection:1 service"); } else if ((WANIPConnection && !WANPPPConnection) && wanIPSrv == null) { throw new UnsupportedOperationException("Unable to find any urn:schemas-upnp-org:service:WANIPConnection:1 service"); } else if ((!WANIPConnection && WANPPPConnection) && wanPPPSrv == null) { throw new UnsupportedOperationException("Unable to find any urn:schemas-upnp-org:service:WANPPPConnection:1 service"); } if (wanIPSrv != null && wanPPPSrv == null) { msgFactory = UPNPMessageFactory.getNewInstance(wanIPSrv); } else if (wanPPPSrv != null && wanIPSrv == null) { msgFactory = UPNPMessageFactory.getNewInstance(wanPPPSrv); } else { // Unable to test the following code since no router implementing both IP and PPP connection on hands.. // // discover the active WAN interface using the WANCommonInterfaceConfig specs UPNPDevice wanDevice = // igd.getChildDevice("urn:schemas-upnp-org:device:WANDevice:1"); // UPNPService configService = myIGDWANConnDevice.getService("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1"); // if (configService != null) { // Retrieve the first active connection // ServiceAction act = configService.getUPNPServiceAction("GetActiveConnection"); // if (act != null) { // UPNPMessageFactory msg = UPNPMessageFactory.getNewInstance(configService); // String deviceContainer = null; // String serviceID = null; // try { // always lookup for the first index of active connections. // ActionResponse resp = msg.getMessage("GetActiveConnection").setInputParameter("NewActiveConnectionIndex", 0).service(); // deviceContainer = resp.getOutActionArgumentValue("NewActiveConnDeviceContainer"); // serviceID = resp.getOutActionArgumentValue("NewActiveConnectionServiceID"); // } catch (IOException ex) { // no response returned // } catch (UPNPResponseException respEx) { // should never happen unless the damn thing is bugged // } // if (deviceContainer != null && deviceContainer.trim().length() > 0 && serviceID != null && serviceID.trim().length() > 0) { // for (Iterator<UPNPDevice> i = igd.getChildDevices().iterator(); i.hasNext();) { // UPNPDevice dv = i.next(); // if (deviceContainer.startsWith(dv.getUDN()) && dv.getDeviceType().indexOf(":WANConnectionDevice:") != -1) { // myIGDWANConnDevice = dv; // break; // } // } // msgFactory = UPNPMessageFactory.getNewInstance(myIGDWANConnDevice.getServiceByID(serviceID)); // } // } // } // Doing a tricky test with external IP address, the inactive interface should return a null value or none if (testWANInterface(wanIPSrv)) { msgFactory = UPNPMessageFactory.getNewInstance(wanIPSrv); } else if (testWANInterface(wanPPPSrv)) { msgFactory = UPNPMessageFactory.getNewInstance(wanPPPSrv); } if (msgFactory == null) { // Nothing found using WANCommonInterfaceConfig! IP by default log.warn("Unable to detect active WANIPConnection, dfaulting to urn:schemas-upnp-org:service:WANIPConnection:1"); msgFactory = UPNPMessageFactory.getNewInstance(wanIPSrv); } } } private boolean testWANInterface(UPNPService srv) { UPNPMessageFactory tmp = UPNPMessageFactory.getNewInstance(srv); ActionMessage msg = tmp.getMessage("GetExternalIPAddress"); String ipToParse = null; try { ipToParse = msg.service().getOutActionArgumentValue("NewExternalIPAddress"); } catch (UPNPResponseException ex) { // ok probably not the IP interface } catch (IOException ex) { // not really normal log.warn("IOException occured during device detection", ex); } if (ipToParse != null && ipToParse.length() > 0 && !ipToParse.equals("0.0.0.0")) { try { return InetAddress.getByName(ipToParse) != null; } catch (UnknownHostException ex) { // ok a crappy IP provided, definitely the wrong interface.. } } return false; } /** * Retreives the IDG UNPNRootDevice object * * @return the UNPNRootDevie object bound to this object */ public UPNPRootDevice getIGDRootDevice() { return igd; } /** * Lookup all the IGD (IP or PPP) devices on the network. If a device implements both IP and PPP, the active service * will be used for nat mappings. * * @param timeout * the timeout in ms to listen for devices response, -1 for default value * @return an array of devices to play with or null if nothing found. * @throws IOException * if some IO Exception occurs during discovery */ public static InternetGatewayDevice[] getDevices(int timeout) throws IOException { return lookupDeviceDevices(timeout, Discovery.DEFAULT_TTL, Discovery.DEFAULT_MX, true, true, null); } /** * Lookup all the IGD (IP urn:schemas-upnp-org:service:WANIPConnection:1, or PPP * urn:schemas-upnp-org:service:WANPPPConnection:1) devices for a given network interface. If a device implements * both IP and PPP, the active service will be used for nat mappings. * * @param timeout * the timeout in ms to listen for devices response, -1 for default value * @param ttl * the discovery ttl such as {@link net.sbbi.upnp.Discovery#DEFAULT_TTL} * @param mx * the discovery mx such as {@link net.sbbi.upnp.Discovery#DEFAULT_MX} * @param ni * the network interface where to lookup IGD devices * @return an array of devices to play with or null if nothing found. * @throws IOException * if some IO Exception occurs during discovery */ public static InternetGatewayDevice[] getDevices(int timeout, int ttl, int mx, NetworkInterface ni) throws IOException { return lookupDeviceDevices(timeout, ttl, mx, true, true, ni); } /** * Lookup all the IGD IP devices on the network (urn:schemas-upnp-org:service:WANIPConnection:1 service) * * @param timeout * the timeout in ms to listen for devices response, -1 for default value * @return an array of devices to play with or null if nothing found or if found devices do not have the * urn:schemas-upnp-org:service:WANIPConnection:1 service * @deprecated use generic {@link #getDevices(int)} or {@link #getDevices(int, int, int, NetworkInterface)} methods * since this one is not usable with all IGD devices ( will only work with devices implementing the * urn:schemas-upnp-org:service:WANIPConnection:1 service ) */ @Deprecated public static InternetGatewayDevice[] getIPDevices(int timeout) throws IOException { return lookupDeviceDevices(timeout, Discovery.DEFAULT_TTL, Discovery.DEFAULT_MX, true, false, null); } /** * Lookup all the IGD PPP devices on the network (urn:schemas-upnp-org:service:WANPPPConnection:1 service) * * @param timeout * the timeout in ms to listen for devices response, -1 for default value * @return an array of devices to play with or null if nothing found or if found devices do not have the * urn:schemas-upnp-org:service:WANPPPConnection:1 service * @deprecated use generic {@link #getDevices(int)} or {@link #getDevices(int, int, int, NetworkInterface)} methods * since this one is not usable with all IGD devices ( will only work with devices implementing the * urn:schemas-upnp-org:service:WANPPPConnection:1 service ) */ @Deprecated public static InternetGatewayDevice[] getPPPDevices(int timeout) throws IOException { return lookupDeviceDevices(timeout, Discovery.DEFAULT_TTL, Discovery.DEFAULT_MX, false, true, null); } private static InternetGatewayDevice[] lookupDeviceDevices(int timeout, int ttl, int mx, boolean WANIPConnection, boolean WANPPPConnection, NetworkInterface ni) throws IOException { UPNPRootDevice[] devices = null; InternetGatewayDevice[] rtrVal = null; if (timeout == -1) { devices = Discovery.discover(Discovery.DEFAULT_TIMEOUT, ttl, mx, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ni); } else { devices = Discovery.discover(timeout, ttl, mx, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ni); } if (devices != null) { Set<InternetGatewayDevice> valid = new HashSet<InternetGatewayDevice>(); for (int i = 0; i < devices.length; i++) { try { valid.add(new InternetGatewayDevice(devices[i], WANIPConnection, WANPPPConnection)); } catch (UnsupportedOperationException ex) { // the device is either not IP or PPP if (log.isDebugEnabled()) log.debug("UnsupportedOperationException during discovery " + ex.getMessage()); } } if (valid.isEmpty()) { return null; } rtrVal = new InternetGatewayDevice[valid.size()]; int i = 0; for (Iterator<InternetGatewayDevice> itr = valid.iterator(); itr.hasNext();) { rtrVal[i++] = itr.next(); } } return rtrVal; } /** * Retrieves the external IP address * * @return a String representing the external IP * @throws UPNPResponseException * if the devices returns an error code * @throws IOException * if some error occurs during communication with the device */ public String getExternalIPAddress() throws UPNPResponseException, IOException { ActionMessage msg = msgFactory.getMessage("GetExternalIPAddress"); return msg.service().getOutActionArgumentValue("NewExternalIPAddress"); } /** * Retrieves a generic port mapping entry. * * @param newPortMappingIndex * the index to lookup in the nat table of the upnp device * @return an action response Object containing the following fields : NewRemoteHost, NewExternalPort, NewProtocol, * NewInternalPort, NewInternalClient, NewEnabled, NewPortMappingDescription, NewLeaseDuration or null if * the index does not exists * @throws IOException * if some error occurs during communication with the device * @throws UPNPResponseException * if some unexpected error occurs on the UPNP device */ public ActionResponse getGenericPortMappingEntry(int newPortMappingIndex) throws IOException, UPNPResponseException { ActionMessage msg = msgFactory.getMessage("GetGenericPortMappingEntry"); msg.setInputParameter("NewPortMappingIndex", newPortMappingIndex); try { return msg.service(); } catch (UPNPResponseException ex) { if (ex.getDetailErrorCode() == 714) { return null; } throw ex; } } /** * Retrieves information about a specific port mapping * * @param remoteHost * the remote host ip to check, null if wildcard * @param externalPort * the port to check * @param protocol * the protocol for the mapping, either TCP or UDP * @return an action response Object containing the following fields : NewInternalPort, NewInternalClient, * NewEnabled, NewPortMappingDescription, NewLeaseDuration or null if no such entry exists in the device NAT * table * @throws IOException * if some error occurs during communication with the device * @throws UPNPResponseException * if some unexpected error occurs on the UPNP device */ public ActionResponse getSpecificPortMappingEntry(String remoteHost, int externalPort, String protocol) throws IOException, UPNPResponseException { remoteHost = remoteHost == null ? "" : remoteHost; checkPortMappingProtocol(protocol); checkPortRange(externalPort); ActionMessage msg = msgFactory.getMessage("GetSpecificPortMappingEntry"); msg.setInputParameter("NewRemoteHost", remoteHost) .setInputParameter("NewExternalPort", externalPort) .setInputParameter("NewProtocol", protocol); try { return msg.service(); } catch (UPNPResponseException ex) { if (ex.getDetailErrorCode() == 714) { return null; } throw ex; } } /** * Configures a nat entry on the UPNP device. * * @param description * the mapping description, null for no description * @param remoteHost * the remote host ip for this entry, null for a wildcard value * @param internalPort * the internal client port where data should be redirected * @param externalPort * the external port to open on the UPNP device an map on the internal client, 0 for a wildcard value * @param internalClient * the internal client ip where data should be redirected * @param leaseDuration * the lease duration in seconds 0 for an infinite time * @param protocol * the protocol, either TCP or UDP * @return true if the port is mapped false if the mapping is allready done for another internal client * @throws IOException * if some error occurs during communication with the device * @throws UPNPResponseException * if the device does not accept some settings :<br/> * 402 Invalid Args See UPnP Device Architecture section on Control<br/> * 501 Action Failed See UPnP Device Architecture section on Control<br/> * 715 WildCardNotPermittedInSrcIP The source IP address cannot be wild-carded<br/> * 716 WildCardNotPermittedInExtPort The external port cannot be wild-carded <br/> * 724 SamePortValuesRequired Internal and External port values must be the same<br/> * 725 OnlyPermanentLeasesSupported The NAT implementation only supports permanent lease times on port * mappings<br/> * 726 RemoteHostOnlySupportsWildcard RemoteHost must be a wildcard and cannot be a specific IP address * or DNS name<br/> * 727 ExternalPortOnlySupportsWildcard ExternalPort must be a wildcard and cannot be a specific port * value */ public boolean addPortMapping(String description, String remoteHost, int internalPort, int externalPort, String internalClient, int leaseDuration, String protocol) throws IOException, UPNPResponseException { remoteHost = remoteHost == null ? "" : remoteHost; checkPortMappingProtocol(protocol); if (externalPort != 0) { checkPortRange(externalPort); } checkPortRange(internalPort); description = description == null ? "" : description; if (leaseDuration < 0) throw new IllegalArgumentException("Invalid leaseDuration (" + leaseDuration + ") value"); ActionMessage msg = msgFactory.getMessage("AddPortMapping"); msg.setInputParameter("NewRemoteHost", remoteHost) .setInputParameter("NewExternalPort", externalPort) .setInputParameter("NewProtocol", protocol) .setInputParameter("NewInternalPort", internalPort) .setInputParameter("NewInternalClient", internalClient) .setInputParameter("NewEnabled", true) .setInputParameter("NewPortMappingDescription", description) .setInputParameter("NewLeaseDuration", leaseDuration); try { msg.service(); return true; } catch (UPNPResponseException ex) { if (ex.getDetailErrorCode() == 718) { return false; } throw ex; } } /** * Deletes a port mapping on the IDG device * * @param remoteHost * the host ip for which the mapping was done, null value for a wildcard value * @param externalPort * the port to close * @param protocol * the protocol for the mapping, TCP or UDP * @return true if the port has been unmapped correctly otherwise false ( entry does not exists ). * @throws IOException * if some error occurs during communication with the device * @throws UPNPResponseException * if the devices returns an error message */ public boolean deletePortMapping(String remoteHost, int externalPort, String protocol) throws IOException, UPNPResponseException { remoteHost = remoteHost == null ? "" : remoteHost; checkPortMappingProtocol(protocol); checkPortRange(externalPort); ActionMessage msg = msgFactory.getMessage("DeletePortMapping"); msg.setInputParameter("NewRemoteHost", remoteHost) .setInputParameter("NewExternalPort", externalPort) .setInputParameter("NewProtocol", protocol); try { msg.service(); return true; } catch (UPNPResponseException ex) { if (ex.getDetailErrorCode() == 714) { return false; } throw ex; } } /** * Retreives the current number of mapping in the NAT table * * @return the nat table current number of mappings or null if the device does not allow to query state variables * @throws IOException * if some error occurs during communication with the device * @throws UPNPResponseException * if the devices returns an error message with error code other than 404 */ public Integer getNatMappingsCount() throws IOException, UPNPResponseException { Integer rtrval = null; StateVariableMessage natTableSize = msgFactory.getStateVariableMessage("PortMappingNumberOfEntries"); try { StateVariableResponse resp = natTableSize.service(); rtrval = new Integer(resp.getStateVariableValue()); } catch (UPNPResponseException ex) { // 404 can happen if device do not implement state variables queries if (ex.getDetailErrorCode() != 404) { throw ex; } } return rtrval; } /** * Computes the total entries in avaliable in the nat table size, not that this method is not guaranteed to work * with all upnp devices since it is not an generic IGD command * * @return the number of entries or null if the NAT table size cannot be computed for the device * @throws IOException * if some error occurs during communication with the device * @throws UPNPResponseException * if the devices returns an error message with error code other than 713 or 402 */ public Integer getNatTableSize() throws IOException, UPNPResponseException { // first let's look at the first index.. some crappy devices do not start with index 0 // we stop at index 50 int startIndex = -1; for (int i = 0; i < 50; i++) { try { this.getGenericPortMappingEntry(i); startIndex = i; break; } catch (UPNPResponseException ex) { // some devices return the 402 code if (ex.getDetailErrorCode() != 713 && ex.getDetailErrorCode() != 402) { throw ex; } } } if (startIndex == -1) { // humm nothing found within the first 50 indexes.. // returning null return null; } int size = 0; while (true) { try { this.getGenericPortMappingEntry(startIndex++); size++; } catch (UPNPResponseException ex) { if (ex.getDetailErrorCode() == 713 || ex.getDetailErrorCode() == 402) { /// ok index unknown break; } throw ex; } } return new Integer(size); } private void checkPortMappingProtocol(String prot) throws IllegalArgumentException { if (prot == null || (!prot.equals("TCP") && !prot.equals("UDP"))) throw new IllegalArgumentException("PortMappingProtocol must be either TCP or UDP"); } private void checkPortRange(int port) throws IllegalArgumentException { if (port < 1 || port > 65535) throw new IllegalArgumentException("Port range must be between 1 and 65535"); } }