/ *
* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 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.yacy.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.yacy.upnp.Discovery ;
import net.yacy.upnp.devices.UPNPDevice ;
import net.yacy.upnp.devices.UPNPRootDevice ;
import net.yacy.upnp.messages.ActionMessage ;
import net.yacy.upnp.messages.ActionResponse ;
import net.yacy.upnp.messages.StateVariableMessage ;
import net.yacy.upnp.messages.StateVariableResponse ;
import net.yacy.upnp.messages.UPNPMessageFactory ;
import net.yacy.upnp.messages.UPNPResponseException ;
import net.yacy.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 . yacy . upnp . Discovery # DEFAULT_TTL }
* @param mx the discovery mx such as { @link net . yacy . 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 ;
}
/ * *
* 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" ) ;
}
}