/**
* weupnp - Trivial upnp java library
*
* Copyright (C) 2008 Alessandro Bahgat Shehata, Daniele Castagna
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.bitlet.weupnp;
import org.bitlet.weupnp.NameValueHandler;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* A <tt>GatewayDevice</tt> is a class that abstracts UPnP-compliant gateways
* <p/>
* It holds all the information that comes back as UPnP responses, and provides
* methods to issue UPnP commands to a gateway.
*
* @author casta
*/
public class GatewayDevice {
/**
* Receive timeout when requesting data from device
*/
private static final int HTTP_RECEIVE_TIMEOUT = 7000;
private String st;
private String location;
private String serviceType;
private String serviceTypeCIF;
private String urlBase;
private String controlURL;
private String controlURLCIF;
private String eventSubURL;
private String eventSubURLCIF;
private String sCPDURL;
private String sCPDURLCIF;
private String deviceType;
private String deviceTypeCIF;
// description data
/**
* The friendly (human readable) name associated with this device
*/
private String friendlyName;
/**
* The device manufacturer name
*/
private String manufacturer;
/**
* The model description as a string
*/
private String modelDescription;
/**
* The URL that can be used to access the IGD interface
*/
private String presentationURL;
/**
* The address used to reach this machine from the GatewayDevice
*/
private InetAddress localAddress;
/**
* The model number (used by the manufacturer to identify the product)
*/
private String modelNumber;
/**
* The model name
*/
private String modelName;
/**
* Creates a new instance of GatewayDevice
*/
public GatewayDevice() {}
/**
* Retrieves the properties and description of the GatewayDevice.
* <p/>
* Connects to the device's {@link #location} and parses the response using
* a {@link GatewayDeviceHandler} to populate the fields of this class
*
* @throws SAXException
* if an error occurs while parsing the request
* @throws IOException
* on communication errors
* @see org.bitlet.weupnp.GatewayDeviceHandler
*/
public void loadDescription() throws SAXException, IOException {
URLConnection urlConn = new URL(getLocation()).openConnection();
urlConn.setReadTimeout(HTTP_RECEIVE_TIMEOUT);
XMLReader parser = XMLReaderFactory.createXMLReader();
parser.setContentHandler(new GatewayDeviceHandler(this));
parser.parse(new InputSource(urlConn.getInputStream()));
/* fix urls */
String ipConDescURL;
if (urlBase != null && urlBase.trim().length() > 0) {
ipConDescURL = urlBase;
} else {
ipConDescURL = location;
}
int lastSlashIndex = ipConDescURL.indexOf('/', 7);
if (lastSlashIndex > 0) {
ipConDescURL = ipConDescURL.substring(0, lastSlashIndex);
}
sCPDURL = copyOrCatUrl(ipConDescURL, sCPDURL);
controlURL = copyOrCatUrl(ipConDescURL, controlURL);
controlURLCIF = copyOrCatUrl(ipConDescURL, controlURLCIF);
presentationURL = copyOrCatUrl(ipConDescURL, presentationURL);
}
/**
* Issues UPnP commands to a GatewayDevice that can be reached at the
* specified <tt>url</tt>
* <p/>
* The command is identified by a <tt>service</tt> and an <tt>action</tt>
* and can receive arguments
*
* @param url
* the url to use to contact the device
* @param service
* the service to invoke
* @param action
* the specific action to perform
* @param args
* the command arguments
* @return the response to the performed command, as a name-value map. In
* case errors occur, the returned map will be <i>empty.</i>
* @throws IOException
* on communication errors
* @throws SAXException
* if errors occur while parsing the response
*/
public static Map<String, String> simpleUPnPcommand(String url, String service, String action,
Map<String, String> args) throws IOException, SAXException {
String soapAction = "\"" + service + "#" + action + "\"";
StringBuilder soapBody = new StringBuilder();
soapBody.append("<?xml version=\"1.0\"?>\r\n" + "<SOAP-ENV:Envelope "
+ "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" "
+ "SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>"
+ "<m:" + action + " xmlns:m=\"" + service + "\">");
if (args != null && args.size() > 0) {
Set<Map.Entry<String, String>> entrySet = args.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
soapBody.append("<" + entry.getKey() + ">" + entry.getValue() + "</" + entry.getKey() + ">");
}
}
soapBody.append("</m:" + action + ">");
soapBody.append("</SOAP-ENV:Body></SOAP-ENV:Envelope>");
URL postUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) postUrl.openConnection();
conn.setRequestMethod("POST");
conn.setReadTimeout(HTTP_RECEIVE_TIMEOUT);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "text/xml");
conn.setRequestProperty("SOAPAction", soapAction);
conn.setRequestProperty("Connection", "Close");
byte[] soapBodyBytes = soapBody.toString().getBytes();
conn.setRequestProperty("Content-Length", String.valueOf(soapBodyBytes.length));
conn.getOutputStream().write(soapBodyBytes);
Map<String, String> nameValue = new HashMap<>();
XMLReader parser = XMLReaderFactory.createXMLReader();
parser.setContentHandler(new NameValueHandler(nameValue));
if (conn.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) {
try {
// attempt to parse the error message
parser.parse(new InputSource(conn.getErrorStream()));
} catch (SAXException e) {
// ignore the exception
// FIXME We probably need to find a better way to return
// significant information when we reach this point
}
conn.disconnect();
return nameValue;
} else {
parser.parse(new InputSource(conn.getInputStream()));
conn.disconnect();
return nameValue;
}
}
/**
* Retrieves the connection status of this device
*
* @return true if connected, false otherwise
* @throws IOException
* @throws SAXException
* @see #simpleUPnPcommand(java.lang.String, java.lang.String,
* java.lang.String, java.util.Map)
*/
public boolean isConnected() throws IOException, SAXException {
Map<String, String> nameValue = simpleUPnPcommand(controlURL, serviceType, "GetStatusInfo", null);
String connectionStatus = nameValue.get("NewConnectionStatus");
if (connectionStatus != null && connectionStatus.equalsIgnoreCase("Connected")) {
return true;
}
return false;
}
/**
* Retrieves the external IP address associated with this device
* <p/>
* The external address is the address that can be used to connect to the
* GatewayDevice from the external network
*
* @return the external IP
* @throws IOException
* @throws SAXException
* @see #simpleUPnPcommand(java.lang.String, java.lang.String,
* java.lang.String, java.util.Map)
*/
public String getExternalIPAddress() throws IOException, SAXException {
Map<String, String> nameValue = simpleUPnPcommand(controlURL, serviceType, "GetExternalIPAddress",
null);
return nameValue.get("NewExternalIPAddress");
}
/**
* Adds a new port mapping to the GatewayDevices using the supplied
* parameters.
*
* @param externalPort
* the external associated with the new mapping
* @param internalPort
* the internal port associated with the new mapping
* @param internalClient
* the internal client associated with the new mapping
* @param protocol
* the protocol associated with the new mapping
* @param description
* the mapping description
* @return true if the mapping was succesfully added, false otherwise
* @throws IOException
* @throws SAXException
* @see #simpleUPnPcommand(java.lang.String, java.lang.String,
* java.lang.String, java.util.Map)
* @see PortMappingEntry
*/
public boolean addPortMapping(int externalPort, int internalPort, String internalClient, String protocol,
String description) throws IOException, SAXException {
Map<String, String> args = new HashMap<>();
args.put("NewRemoteHost", ""); // wildcard, any remote host matches
args.put("NewExternalPort", Integer.toString(externalPort));
args.put("NewProtocol", protocol);
args.put("NewInternalPort", Integer.toString(internalPort));
args.put("NewInternalClient", internalClient);
args.put("NewEnabled", Integer.toString(1));
args.put("NewPortMappingDescription", description);
args.put("NewLeaseDuration", Integer.toString(0));
Map<String, String> nameValue = simpleUPnPcommand(controlURL, serviceType, "AddPortMapping", args);
return nameValue.get("errorCode") == null;
}
/**
* Queries the GatewayDevice to retrieve a specific port mapping entry,
* corresponding to specified criteria, if present.
* <p/>
* Retrieves the <tt>PortMappingEntry</tt> associated with
* <tt>externalPort</tt> and <tt>protocol</tt>, if present.
*
* @param externalPort
* the external port
* @param protocol
* the protocol (TCP or UDP)
* @param portMappingEntry
* the entry containing the details, in any is present, <i>null</i>
* otherwise. <i>(used as return value)</i>
* @return true if a valid mapping is found
* @throws IOException
* @throws SAXException
* @todo consider refactoring this method to make it consistent with Java
* practices (return the port mapping)
* @see #simpleUPnPcommand(java.lang.String, java.lang.String,
* java.lang.String, java.util.Map)
* @see PortMappingEntry
*/
public boolean getSpecificPortMappingEntry(int externalPort, String protocol,
final PortMappingEntry portMappingEntry) throws IOException, SAXException {
portMappingEntry.setExternalPort(externalPort);
portMappingEntry.setProtocol(protocol);
Map<String, String> args = new HashMap<>();
args.put("NewRemoteHost", ""); // wildcard, any remote host matches
args.put("NewExternalPort", Integer.toString(externalPort));
args.put("NewProtocol", protocol);
Map<String, String> nameValue = simpleUPnPcommand(controlURL, serviceType,
"GetSpecificPortMappingEntry", args);
if (nameValue.isEmpty() || nameValue.containsKey("errorCode"))
return false;
if (!nameValue.containsKey("NewInternalClient") || !nameValue.containsKey("NewInternalPort"))
return false;
portMappingEntry.setProtocol(nameValue.get("NewProtocol"));
portMappingEntry.setEnabled(nameValue.get("NewEnabled"));
portMappingEntry.setInternalClient(nameValue.get("NewInternalClient"));
portMappingEntry.setExternalPort(externalPort);
portMappingEntry.setPortMappingDescription(nameValue.get("NewPortMappingDescription"));
portMappingEntry.setRemoteHost(nameValue.get("NewRemoteHost"));
try {
portMappingEntry.setInternalPort(Integer.parseInt(nameValue.get("NewInternalPort")));
} catch (NumberFormatException nfe) {
// skip bad port
}
return true;
}
/**
* Returns a specific port mapping entry, depending on a the supplied index.
*
* @param index
* the index of the desired port mapping
* @param portMappingEntry
* the entry containing the details, in any is present, <i>null</i>
* otherwise. <i>(used as return value)</i>
* @return true if a valid mapping is found
* @throws IOException
* @throws SAXException
* @todo consider refactoring this method to make it consistent with Java
* practices (return the port mapping)
* @see #simpleUPnPcommand(java.lang.String, java.lang.String,
* java.lang.String, java.util.Map)
* @see PortMappingEntry
*/
public boolean getGenericPortMappingEntry(int index, final PortMappingEntry portMappingEntry)
throws IOException, SAXException {
Map<String, String> args = new HashMap<>();
args.put("NewPortMappingIndex", Integer.toString(index));
Map<String, String> nameValue = simpleUPnPcommand(controlURL, serviceType,
"GetGenericPortMappingEntry", args);
if (nameValue.isEmpty() || nameValue.containsKey("errorCode"))
return false;
portMappingEntry.setRemoteHost(nameValue.get("NewRemoteHost"));
portMappingEntry.setInternalClient(nameValue.get("NewInternalClient"));
portMappingEntry.setProtocol(nameValue.get("NewProtocol"));
portMappingEntry.setEnabled(nameValue.get("NewEnabled"));
portMappingEntry.setPortMappingDescription(nameValue.get("NewPortMappingDescription"));
try {
portMappingEntry.setInternalPort(Integer.parseInt(nameValue.get("NewInternalPort")));
} catch (Exception e) {}
try {
portMappingEntry.setExternalPort(Integer.parseInt(nameValue.get("NewExternalPort")));
} catch (Exception e) {}
return true;
}
/**
* Retrieves the number of port mappings that are registered on the
* GatewayDevice.
*
* @return the number of port mappings
* @throws IOException
* @throws SAXException
*/
public Integer getPortMappingNumberOfEntries() throws IOException, SAXException {
Map<String, String> nameValue = simpleUPnPcommand(controlURL, serviceType,
"GetPortMappingNumberOfEntries", null);
Integer portMappingNumber = null;
try {
portMappingNumber = Integer.valueOf(nameValue.get("NewPortMappingNumberOfEntries"));
} catch (Exception e) {}
return portMappingNumber;
}
/**
* Deletes the port mapping associated to <tt>externalPort</tt> and
* <tt>protocol</tt>
*
* @param externalPort
* the external port
* @param protocol
* the protocol
* @return true if removal was successful
* @throws IOException
* @throws SAXException
*/
public boolean deletePortMapping(int externalPort, String protocol) throws IOException, SAXException {
Map<String, String> args = new HashMap<>();
args.put("NewRemoteHost", "");
args.put("NewExternalPort", Integer.toString(externalPort));
args.put("NewProtocol", protocol);
@SuppressWarnings("unused")
Map<String, String> nameValue = simpleUPnPcommand(controlURL, serviceType, "DeletePortMapping", args);
return true;
}
// getters and setters
/**
* Gets the local address to connect the gateway through
*
* @return the {@link #localAddress}
*/
public InetAddress getLocalAddress() {
return localAddress;
}
/**
* Sets the {@link #localAddress}
*
* @param localAddress
* the address to set
*/
public void setLocalAddress(InetAddress localAddress) {
this.localAddress = localAddress;
}
public String getSt() {
return st;
}
public void setSt(String st) {
this.st = st;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getServiceType() {
return serviceType;
}
public void setServiceType(String serviceType) {
this.serviceType = serviceType;
}
public String getServiceTypeCIF() {
return serviceTypeCIF;
}
public void setServiceTypeCIF(String serviceTypeCIF) {
this.serviceTypeCIF = serviceTypeCIF;
}
public String getControlURL() {
return controlURL;
}
public void setControlURL(String controlURL) {
this.controlURL = controlURL;
}
public String getControlURLCIF() {
return controlURLCIF;
}
public void setControlURLCIF(String controlURLCIF) {
this.controlURLCIF = controlURLCIF;
}
public String getEventSubURL() {
return eventSubURL;
}
public void setEventSubURL(String eventSubURL) {
this.eventSubURL = eventSubURL;
}
public String getEventSubURLCIF() {
return eventSubURLCIF;
}
public void setEventSubURLCIF(String eventSubURLCIF) {
this.eventSubURLCIF = eventSubURLCIF;
}
public String getSCPDURL() {
return sCPDURL;
}
public void setSCPDURL(String sCPDURL) {
this.sCPDURL = sCPDURL;
}
public String getSCPDURLCIF() {
return sCPDURLCIF;
}
public void setSCPDURLCIF(String sCPDURLCIF) {
this.sCPDURLCIF = sCPDURLCIF;
}
public String getDeviceType() {
return deviceType;
}
public void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
public String getDeviceTypeCIF() {
return deviceTypeCIF;
}
public void setDeviceTypeCIF(String deviceTypeCIF) {
this.deviceTypeCIF = deviceTypeCIF;
}
public String getURLBase() {
return urlBase;
}
public void setURLBase(String uRLBase) {
this.urlBase = uRLBase;
}
public String getFriendlyName() {
return friendlyName;
}
public void setFriendlyName(String friendlyName) {
this.friendlyName = friendlyName;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public String getModelDescription() {
return modelDescription;
}
public void setModelDescription(String modelDescription) {
this.modelDescription = modelDescription;
}
public String getPresentationURL() {
return presentationURL;
}
public void setPresentationURL(String presentationURL) {
this.presentationURL = presentationURL;
}
public String getModelName() {
return modelName;
}
public void setModelName(String modelName) {
this.modelName = modelName;
}
public String getModelNumber() {
return modelNumber;
}
public void setModelNumber(String modelNumber) {
this.modelNumber = modelNumber;
}
// private methods
private String copyOrCatUrl(String dst, String src) {
if (src != null) {
if (src.startsWith("http://")) {
dst = src;
} else {
if (!src.startsWith("/")) {
dst += "/";
}
dst += src;
}
}
return dst;
}
}