/*
* ====================================================================
* ======== 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.tomp2p.upnp;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* This class can be used to access some functionalities 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 RootDevice igd;
private UPNPMessageFactory msgFactory;
private InternetGatewayDevice(RootDevice igd, boolean WANIPConnection, boolean WANPPPConnection)
throws UnsupportedOperationException {
this.igd = igd;
Device 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.modelName);
}
Service wanIPSrv = myIGDWANConnDevice.getService("urn:schemas-upnp-org:service:WANIPConnection:1");
Service 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 = new UPNPMessageFactory(wanIPSrv);
} else if (wanPPPSrv != null && wanIPSrv == null) {
msgFactory = new UPNPMessageFactory(wanPPPSrv);
} else {
// discover the active WAN interface using the
// WANCommonInterfaceConfig specs
Device wanDevice = igd.getChildDevice("urn:schemas-upnp-org:device:WANDevice:1");
Service configService = wanDevice.getService("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1");
if (configService != null) { // retrieve the first active connection
Action act = configService.getUPNPServiceAction("GetActiveConnection");
if (act != null) { // this action is optional - definitely not
// supported by
// the NetGear WGR614v9
UPNPMessageFactory msg = new UPNPMessageFactory(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<Device> i = igd.getChildDevices().iterator(); i.hasNext();) {
Device dv = i.next();
if (deviceContainer.startsWith(dv.UDN)
&& dv.deviceType.indexOf(":WANConnectionDevice:") != -1) {
myIGDWANConnDevice = dv;
break;
}
}
msgFactory = new UPNPMessageFactory(myIGDWANConnDevice.getServiceByID(serviceID));
}
} else {
// Lets look at the required WANAccessType variable
StateVariable wat = configService.getUPNPServiceStateVariable("WANAccessType");
assert wat != null : "bugged upnp implementation";
try {
String accessType = wat.getValue();
System.out.println(accessType);
if (accessType.equals("DSL") || accessType.equals("POTS")) {
msgFactory = new UPNPMessageFactory(wanPPPSrv);
} else if (accessType.equals("Cable") || accessType.equals("Ethernet")) {
msgFactory = new UPNPMessageFactory(wanIPSrv);
} else {
assert false : "Illegal access type : " + accessType;
}
} catch (UPNPResponseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
if (msgFactory == null) { // last-ditch attempt
// Doing a tricky test with external IP
// address, the
// unactive
// interface should return a null value
// or none
if (testWANInterface(wanIPSrv)) {
msgFactory = new UPNPMessageFactory(wanIPSrv);
} else if (testWANInterface(wanPPPSrv)) {
msgFactory = new UPNPMessageFactory(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 = new UPNPMessageFactory(wanIPSrv);
}
}
}
private boolean testWANInterface(Service srv) {
UPNPMessageFactory tmp = new UPNPMessageFactory(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 occurred 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;
}
/**
* Retrieves the IDG UNPNRootDevice object
*
* @return the UNPNRootDevie object bound to this object
*/
public RootDevice 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 Collection<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.tomp2p.upnp.Discovery#DEFAULT_TTL}
* @param mx
* the discovery mx such as
* {@link net.tomp2p.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 Collection<InternetGatewayDevice> getDevices(int timeout, int ttl, int mx, NetworkInterface ni)
throws IOException {
return lookupDeviceDevices(timeout, ttl, mx, true, true, ni);
}
private static Collection<InternetGatewayDevice> lookupDeviceDevices(int timeout, int ttl, int mx,
boolean WANIPConnection, boolean WANPPPConnection, NetworkInterface ni) throws IOException {
Collection<RootDevice> devices = Discovery.discover(timeout == -1 ? Discovery.DEFAULT_TIMEOUT : timeout, ttl,
mx, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ni);
if (devices == null)
return null;
Set<InternetGatewayDevice> valid = new HashSet<InternetGatewayDevice>();
for (RootDevice device : devices) {
try {
valid.add(new InternetGatewayDevice(device, WANIPConnection, WANPPPConnection));
} catch (UnsupportedOperationException ex) {
// the device is either not IP or PPP
// log.debug(
// "UnsupportedOperationException during discovery " +
// ex.getMessage() );
}
}
if (valid.size() == 0) {
return null;
}
return valid;
}
/**
* 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;
}
}
public boolean addPortMapping(String description, String protocol, String internalHost, int externalPort,
int internalPort) throws IOException, UPNPResponseException {
// we need to be sure that igd.getLocalIP().getHostAddress() is in the
// network of internalHost
return addPortMapping(description, protocol, null, externalPort, internalHost, internalPort, 0);
}
/**
* Configures a nat entry on the UPNP device.
*
* @param description
* the mapping description, null for no description
* @param protocol
* the protocol, either TCP or UDP
* @param remoteHost
* the remote host ip for this entry, null for a wildcard value
* @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 internalPort
* the internal client port where data should be redirected
* @param leaseDuration
* the lease duration in seconds 0 for an infinite time
* @return true if the port is mapped false if the mapping is already 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 protocol, String remoteHost, int externalPort,
String internalClient, int internalPort, int leaseDuration) 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;
}
}
/**
* Retrieves 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;
}
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");
}
}
}