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