/* * ============================================================================ * The Apache Software License, Version 1.1 * ============================================================================ * * Copyright (C) 2002 The Apache Software Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without modifica- * tion, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The end-user documentation included with the redistribution, if any, must * include the following acknowledgment: "This product includes software * developed by SuperBonBon Industries (http://www.sbbi.net/)." * Alternately, this acknowledgment may appear in the software itself, if * and wherever such third-party acknowledgments normally appear. * * 4. The names "UPNPLib" and "SuperBonBon Industries" must not be * used to endorse or promote products derived from this software without * prior written permission. For written permission, please contact * info@sbbi.net. * * 5. Products derived from this software may not be called * "SuperBonBon Industries", nor may "SBBI" appear in their name, * without prior written permission of SuperBonBon Industries. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT,INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- * DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals * on behalf of SuperBonBon Industries. For more information on * SuperBonBon Industries, please see <http://www.sbbi.net/>. */ 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 org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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; /** * 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 Log log = LogFactory.getLog( InternetGatewayDevice.class ); private 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 = wanDevice.getService( "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" ); if ( configService != null ) { // retreive 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 i = igd.getChildDevices().iterator(); i.hasNext(); ) { UPNPDevice dv = (UPNPDevice)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 unactive 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, definitly 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 ) */ 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 ) */ 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 valid = new HashSet(); 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.size() == 0 ) { return null; } rtrVal = new InternetGatewayDevice[valid.size()]; int i = 0; for ( Iterator itr = valid.iterator(); itr.hasNext(); ) { rtrVal[i++] = (InternetGatewayDevice)itr.next(); } } return rtrVal; } /** * Retreives 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" ); } /** * Retreives 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; } } /** * Retreives 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 200 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" ); } }