/*
This file is part of jpcsp.
Jpcsp is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Jpcsp 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Jpcsp. If not, see <http://www.gnu.org/licenses/>.
*/
package jpcsp.network.upnp;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.HttpURLConnection;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import jpcsp.State;
import jpcsp.util.Utilities;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class UPnP {
public static Logger log = Logger.getLogger("upnp");
protected IGD igd;
public static final int discoveryTimeoutMillis = 2000;
public static final int discoveryPort = 1900;
public static final int discoverySearchPort = 1901;
public static final String multicastIp = "239.255.255.250";
private static final String[] deviceList = new String[] {
"urn:schemas-upnp-org:device:InternetGatewayDevice:1",
"urn:schemas-upnp-org:service:WANIPConnection:1",
"urn:schemas-upnp-org:service:WANPPPConnection:1",
"upnp:rootdevice"
};
private class DiscoverThread extends Thread {
@Override
public void run() {
discover();
}
}
public void discoverInBackground() {
DiscoverThread discoverThread = new DiscoverThread();
discoverThread.setName("UPnP Discover Thread");
discoverThread.setDaemon(true);
discoverThread.start();
}
private static class ListenerThread extends Thread {
private UPnP upnp;
private IGD igd;
private boolean done;
private volatile boolean ready;
public ListenerThread(UPnP upnp, IGD igd) {
this.upnp = upnp;
this.igd = igd;
}
@Override
public void run() {
MulticastSocket sockets[] = new MulticastSocket[100];
int numberSockets = 0;
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements() && numberSockets < sockets.length) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
if (networkInterface.isUp() && networkInterface.supportsMulticast() && !networkInterface.isLoopback()) {
for (Enumeration<InetAddress> addresses = networkInterface.getInetAddresses(); addresses.hasMoreElements() && numberSockets < sockets.length; ) {
InetAddress address = addresses.nextElement();
if (address instanceof Inet4Address && !address.isLoopbackAddress()) {
sockets[numberSockets] = new MulticastSocket(new InetSocketAddress(address, discoverySearchPort));
sockets[numberSockets].setSoTimeout(1);
numberSockets++;
}
}
}
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Set<String> processedUrls = new HashSet<String>();
ready = true;
byte[] buffer = new byte[1536];
while (!isDone()) {
for (int i = 0; i < numberSockets && !isDone(); i++) {
try {
DatagramPacket responsePacket = new DatagramPacket(buffer, buffer.length);
sockets[i].receive(responsePacket);
if (responsePacket.getLength() > 0) {
String reply = new String(responsePacket.getData(), responsePacket.getOffset(), responsePacket.getLength());
if (log.isDebugEnabled()) {
log.debug(String.format("Discovery: %s", reply));
}
String location = null;
Pattern pLocation = Pattern.compile("^location: *(\\S+)$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
Matcher mLocation = pLocation.matcher(reply);
if (mLocation.find()) {
location = mLocation.group(1);
}
String st = null;
Pattern pSt = Pattern.compile("^st: *(\\S+)$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
Matcher mSt = pSt.matcher(reply);
if (mSt.find()) {
st = mSt.group(1);
}
if (location != null && st != null) {
if (log.isDebugEnabled()) {
log.debug(String.format("Location: '%s', st: '%s'", location, st));
}
if (!processedUrls.contains(location)) {
igd.discover(location);
processedUrls.add(location);
if (igd.isValid() && igd.isConnected(upnp)) {
if (log.isDebugEnabled()) {
log.debug(String.format("IGD connected with external IP: %s", igd.getExternalIPAddress(upnp)));
}
setDone(true);
}
}
} else {
log.error(String.format("Could not parse discovery response: %s", reply));
}
}
} catch (SocketTimeoutException e) {
} catch (IOException e) {
}
}
}
for (int i = 0; i < numberSockets; i++) {
sockets[i].disconnect();
sockets[i].close();
}
ready = true;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
ready = false;
}
public boolean isReady() {
return ready;
}
}
public void discover() {
try {
igd = new IGD();
ListenerThread listener = new ListenerThread(this, igd);
listener.setDaemon(true);
listener.setName("UPnP Discovery Listener");
listener.start();
while (!listener.isReady()) {
Utilities.sleep(100);
}
for (String device : deviceList) {
String discoveryRequest = String.format("M-SEARCH * HTTP/1.1\r\nHOST: %s:%d\r\nST: %s\r\nMAN: \"ssdp:discover\"\r\nMX: %d\r\n\r\n", multicastIp, discoveryPort, device, discoveryTimeoutMillis / 1000);
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
if (networkInterface.isUp() && networkInterface.supportsMulticast()) {
for (Enumeration<InetAddress> addresses = networkInterface.getInetAddresses(); addresses.hasMoreElements(); ) {
InetAddress address = addresses.nextElement();
if (address instanceof Inet4Address && !address.isLoopbackAddress()) {
MulticastSocket socket = new MulticastSocket(new InetSocketAddress(address, discoverySearchPort));
InetSocketAddress socketAddress = new InetSocketAddress(multicastIp, discoveryPort);
DatagramPacket packet = new DatagramPacket(discoveryRequest.getBytes(), discoveryRequest.length(), socketAddress);
socket.send(packet);
socket.disconnect();
socket.close();
}
}
}
}
}
for (int i = 0; i < discoveryTimeoutMillis / 10; i++) {
if (listener.isDone()) {
break;
}
Utilities.sleep(10, 0);
}
listener.setDone(true);
while (!listener.isReady()) {
Utilities.sleep(100);
}
} catch (IOException e) {
log.error("discover", e);
}
}
public IGD getIGD() {
return igd;
}
protected HashMap<String, String> executeSimpleUPnPcommand(String controlUrl, String serviceType, String action, HashMap<String, String> arguments) {
HashMap<String, String> result = null;
StringBuilder body = new StringBuilder();
body.append(String.format("<?xml version=\"1.0\"?>\r\n"));
body.append(String.format("<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n"));
body.append(String.format("<s:Body>\r\n"));
body.append(String.format(" <u:%s xmlns:u=\"%s\">\r\n", action, serviceType));
if (arguments != null) {
for (String name : arguments.keySet()) {
String value = arguments.get(name);
if (value == null || value.isEmpty()) {
body.append(String.format(" <%s />\r\n", name));
} else {
body.append(String.format(" <%s>%s</%s>\r\n", name, value, name));
}
}
}
body.append(String.format(" </u:%s>\r\n", action));
body.append(String.format("</s:Body>\r\n"));
body.append(String.format("</s:Envelope>\r\n"));
if (log.isTraceEnabled()) {
log.trace(String.format("Sending UPnP command: %s", body.toString()));
}
byte[] bodyBytes = body.toString().getBytes();
try {
URL url = new URL(controlUrl);
URLConnection connection = url.openConnection();
if (connection instanceof HttpURLConnection) {
HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
httpURLConnection.setRequestMethod("POST");
}
connection.setRequestProperty("SOAPAction", String.format("%s#%s", serviceType, action));
connection.setRequestProperty("Content-Type", "text/xml");
connection.setRequestProperty("Content-Length", Integer.toString(bodyBytes.length));
connection.setDoOutput(true);
OutputStream output = connection.getOutputStream();
output.write(bodyBytes);
output.flush();
output.close();
connection.connect();
InputStream response = connection.getInputStream();
StringBuilder content = new StringBuilder();
byte[] buffer = new byte[1024];
int n;
do {
n = response.read(buffer);
if (n > 0) {
content.append(new String(buffer, 0, n));
}
} while (n >= 0);
response.close();
if (log.isDebugEnabled()) {
log.debug(String.format("UPnP command serviceType %s, action %s, result: %s", serviceType, action, content.toString()));
}
result = parseSimpleCommandResponse(content.toString());
if (log.isDebugEnabled()) {
String errorCode = result.get("errorCode");
if (errorCode != null) {
log.debug(String.format("UPnP command %s: errorCode = %s", action, errorCode));
}
}
} catch (MalformedURLException e) {
log.error("executeUPnPcommand", e);
} catch (IOException e) {
log.error("executeUPnPcommand", e);
}
return result;
}
protected HashMap<String, String> parseSimpleCommandResponse(String content) {
HashMap<String, String> result = null;
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setIgnoringElementContentWhitespace(true);
documentBuilderFactory.setIgnoringComments(true);
documentBuilderFactory.setCoalescing(true);
try {
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document response = documentBuilder.parse(new ByteArrayInputStream(content.getBytes()));
result = new HashMap<String, String>();
parseElement(response.getDocumentElement(), result, null);
} catch (ParserConfigurationException e) {
log.error("Discovery", e);
} catch (SAXException e) {
log.error("Discovery", e);
} catch (MalformedURLException e) {
log.error("Discovery", e);
} catch (IOException e) {
log.error("Discovery", e);
}
return result;
}
protected void parseElement(Element element, HashMap<String, String> result, String name) {
NodeList children = element.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node instanceof Element) {
parseElement((Element) node, result, node.getNodeName());
} else if (name != null && node.getTextContent() != null) {
String value = node.getTextContent();
if (result.containsKey(name)) {
value = result.get(name) + value;
}
result.put(name, value);
}
}
}
public String getStatusInfo(String controlUrl, String serviceType) {
HashMap<String, String> result = executeSimpleUPnPcommand(controlUrl, serviceType, "GetStatusInfo", null);
return result.get("NewConnectionStatus");
}
public String getExternalIPAddress(String controlUrl, String serviceType) {
HashMap<String, String> result = executeSimpleUPnPcommand(controlUrl, serviceType, "GetExternalIPAddress", null);
return result.get("NewExternalIPAddress");
}
public void addPortMapping(String controlUrl, String serviceType, String remoteHost, int externalPort, String protocol, int internalPort, String internalClient, String description, int leaseDuration) {
HashMap<String, String> arguments = new HashMap<String, String>();
arguments.put("NewRemoteHost", remoteHost);
arguments.put("NewExternalPort", Integer.toString(externalPort));
arguments.put("NewProtocol", getProtocol(protocol));
arguments.put("NewInternalPort", Integer.toString(internalPort));
arguments.put("NewInternalClient", internalClient);
arguments.put("NewEnabled", "1");
arguments.put("NewPortMappingDescription", description != null ? description : String.format("Jpcsp-%s", State.discId));
arguments.put("NewLeaseDuration", Integer.toString(leaseDuration));
HashMap<String, String> result = executeSimpleUPnPcommand(controlUrl, serviceType, "AddPortMapping", arguments);
if (log.isDebugEnabled() && result != null) {
log.debug(String.format("addPortMapping errorCode=%s", result.get("errorCode")));
}
}
public void deletePortMapping(String controlUrl, String serviceType, String remoteHost, int externalPort, String protocol) {
HashMap<String, String> arguments = new HashMap<String, String>();
arguments.put("NewRemoteHost", remoteHost);
arguments.put("NewExternalPort", Integer.toString(externalPort));
arguments.put("NewProtocol", getProtocol(protocol));
HashMap<String, String> result = executeSimpleUPnPcommand(controlUrl, serviceType, "DeletePortMapping", arguments);
if (log.isDebugEnabled() && result != null) {
log.debug(String.format("deletePortMapping errorCode=%s", result.get("errorCode")));
}
}
protected String getProtocol(String protocol) {
if (protocol != null) {
protocol = protocol.toUpperCase();
if (!protocol.equals("TCP") && !protocol.equals("UDP")) {
// Unknown protocol
protocol = null;
}
}
return protocol;
}
}