/*
* 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");
}
}