/*
* 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.remote;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.ExportException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.RemoteRef;
import java.rmi.server.RemoteServer;
import java.rmi.server.RemoteStub;
import java.rmi.server.ServerCloneException;
import java.rmi.server.ServerRef;
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.UPNPMessageFactory;
/**
* This class can be used for remote objects that need to work behind an NAT firewall compatible with IGD UPNP
* specifications. The following system properties let you setup this class : <br/>
*
* net.sbbi.upnp.remote.deviceUDN=someUPNPDeviceUDN, the device identifier to be used when multiple IGD upnp devices are
* on the network<br/>
* net.sbbi.upnp.remote.failWhenNoDeviceFound=true|false, Property to throw an exception when the object is exported and
* no UPNP device is found, default to false<br/>
* net.sbbi.upnp.remote.failWhenDeviceCommEx=true|false, Property to throw an exception when the object is exported and
* an error occurs during com with device, default to false<br/>
* net.sbbi.upnp.remote.discoveryTimeout=4000, timeout in ms to discover upnp devices default to 1500, try to increase
* this timeout if you can't find a present device on the network<br/>
*
* Each instance of this class can create a shutdown hook trigered during JVM shutdown to make sure that the port opened
* with UPNP is closed. The hook is created as soon as the port is opened on the UPNP device.<br/>
*
* Migration for distributed objects is quite simple : change the standard java.rmi.server.UnicastRemoteObject class
* extends to this class and you're done.<br/>
*
* If you have trouble to make the objects available from behind your router/firewall make sure that you have correctly
* set the java.rmi.server.hostname system property with an hostname matching your router/firewall IP.<br/>
*
* Make also sure that your RMI Registry port is opened on the router otherwise nothing will work. You can use a
* urn:schemas-upnp-org:device:InternetGatewayDevice:1 device just like this class to automate the job.
*
* @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a>
* @version 1.0
*/
public class UnicastRemoteObject extends RemoteServer {
/* indicate compatibility with JDK 1.1.x version of class */
private static final long serialVersionUID = 4974527148936298034L;
/* parameter types for server ref constructor invocation used below */
private static Class<?>[] portParamTypes = { int.class };
/* parameter types for server ref constructor invocation used below */
private static Class<?>[] portFactoryParamTypes = {
int.class, RMIClientSocketFactory.class, RMIServerSocketFactory.class
};
private final static Object DISCOVERY_PROCESS = new Object();
private final static Object ANONYMOUS_PORT_LOOKUP = new Object();
private UPNPDevice wanConnDevice = null;
private boolean discoveryProcessCall = false;
private boolean portOpened = false;
/**
* @serial port number on which to export object
*/
private int port = 0;
/**
* @serial client-side socket factory (if any)
*/
private RMIClientSocketFactory csf = null;
/**
* @serial server-side socket factory (if any) to use when exporting object
*/
private RMIServerSocketFactory ssf = null;
/**
* Creates and exports a new UnicastRemoteObject object using an anonymous port.
*
* @throws RemoteException
* if failed to export object
*/
protected UnicastRemoteObject() throws RemoteException {
// let's open a server socket with an anonymous port
synchronized (ANONYMOUS_PORT_LOOKUP) {
try {
ServerSocket srv = new ServerSocket(0);
this.port = srv.getLocalPort();
srv.close();
} catch (Exception ex) {
throw new RemoteException("Error occured during anonymous port assignation", ex);
}
}
openPort();
exportObject(this, port);
}
/**
* Creates and exports a new UnicastRemoteObject object using the particular supplied port.
*
* @param port
* the port number on which the remote object receives calls (if <code>port</code> is zero, an anonymous
* port is chosen)
* @throws RemoteException
* if failed to export object
*/
protected UnicastRemoteObject(int port) throws RemoteException {
this.port = port;
openPort();
exportObject(this, port);
}
/**
* Creates and exports a new UnicastRemoteObject object using the particular supplied port and socket factories.
*
* @param port
* the port number on which the remote object receives calls (if <code>port</code> is zero, an anonymous
* port is chosen)
* @param csf
* the client-side socket factory for making calls to the remote object
* @param ssf
* the server-side socket factory for receiving remote calls
* @throws RemoteException
* if failed to export object
*/
protected UnicastRemoteObject(int port, RMIClientSocketFactory csf,
RMIServerSocketFactory ssf) throws RemoteException {
this.port = port;
this.csf = csf;
this.ssf = ssf;
openPort();
exportObject(this, port, csf, ssf);
}
/**
* Re-export the remote object when it is deserialized.
*/
private void readObject(java.io.ObjectInputStream in)
throws java.io.IOException,
java.lang.ClassNotFoundException {
in.defaultReadObject();
reexport();
}
/**
* Returns a clone of the remote object that is distinct from the original.
*
* @exception CloneNotSupportedException
* if clone failed due to a RemoteException.
* @return the new remote object
*/
@Override
public Object clone() throws CloneNotSupportedException {
try {
UnicastRemoteObject cloned = (UnicastRemoteObject) super.clone();
cloned.reexport();
return cloned;
} catch (RemoteException e) {
throw new ServerCloneException("Clone failed", e);
}
}
/**
* Exports this UnicastRemoteObject using its initialized fields because its creation bypassed running its
* constructors (via deserialization or cloning, for example).
*/
private void reexport() throws RemoteException {
closePort();
if ((csf == null) && (ssf == null)) {
exportObject(this, port);
} else {
exportObject(this, port, csf, ssf);
}
openPort();
}
/**
* Exports the remote object to make it available to receive incoming calls using an anonymous port.
*
* @param obj
* the remote object to be exported
* @return remote object stub
* @exception RemoteException
* if export fails
*/
public static RemoteStub exportObject(Remote obj) throws RemoteException {
return (RemoteStub) exportObject(obj, 0);
}
/**
* Exports the remote object to make it available to receive incoming calls, using the particular supplied port.
*
* @param obj
* the remote object to be exported
* @param port
* the port to export the object on
* @return remote object stub
* @exception RemoteException
* if export fails
*/
public static Remote exportObject(Remote obj, int port)
throws RemoteException {
// prepare arguments for server ref constructor
Object[] args = new Object[] { new Integer(port) };
return exportObject(obj, "UnicastServerRef", portParamTypes, args);
}
/**
* Exports the remote object to make it available to receive incoming calls, using a transport specified by the
* given socket factory.
*
* @param obj
* the remote object to be exported
* @param port
* the port to export the object on
* @param csf
* the client-side socket factory for making calls to the remote object
* @param ssf
* the server-side socket factory for receiving remote calls
* @return remote object stub
* @exception RemoteException
* if export fails
*/
public static Remote exportObject(Remote obj, int port,
RMIClientSocketFactory csf,
RMIServerSocketFactory ssf)
throws RemoteException {
// prepare arguments for server ref constructor
Object[] args = new Object[] { new Integer(port), csf, ssf };
return exportObject(obj, "UnicastServerRef2", portFactoryParamTypes,
args);
}
/*
* Creates an instance of given server ref type with constructor chosen by indicated paramters and supplied with
* given arguements, and export remote object with it.
*/
private static Remote exportObject(Remote obj, String refType,
Class<?>[] params, Object[] args)
throws RemoteException {
// compose name of server ref class and find it
String refClassName = RemoteRef.packagePrefix + "." + refType;
Class<?> refClass;
try {
refClass = Class.forName(refClassName);
} catch (ClassNotFoundException e) {
throw new ExportException("No class found for server ref type: " +
refType);
}
if (!ServerRef.class.isAssignableFrom(refClass)) {
throw new ExportException("Server ref class not instance of " +
ServerRef.class.getName() + ": " +
refClass.getName());
}
// create server ref instance using given constructor and arguments
ServerRef serverRef;
try {
java.lang.reflect.Constructor<?> cons = refClass.getConstructor(params);
serverRef = (ServerRef) cons.newInstance(args);
// if impl does extends UnicastRemoteObject, set its ref
if (obj instanceof UnicastRemoteObject)
((UnicastRemoteObject) obj).ref = serverRef;
} catch (Exception e) {
throw new ExportException("Exception creating instance of server ref class: " +
refClass.getName(), e);
}
return serverRef.exportObject(obj, null);
}
private void openPort() throws RemoteException {
discoverDevice();
if (wanConnDevice != null && !portOpened) {
net.sbbi.upnp.services.UPNPService wanIPSrv = wanConnDevice.getService("urn:schemas-upnp-org:service:WANIPConnection:1");
String failStr = System.getProperty("net.sbbi.upnp.remote.failWhenNoDeviceFound");
boolean fail = false;
if (failStr != null && failStr.equalsIgnoreCase("true"))
fail = true;
if (wanIPSrv == null && fail) {
throw new RemoteException("Device does not implement the urn:schemas-upnp-org:service:WANIPConnection:1 service");
} else if (wanIPSrv == null && !fail) {
return;
}
failStr = System.getProperty("net.sbbi.upnp.remote.failWhenDeviceCommEx");
fail = false;
if (failStr != null && failStr.equalsIgnoreCase("true"))
fail = true;
UPNPMessageFactory msgFactory = UPNPMessageFactory.getNewInstance(wanIPSrv);
ActionMessage msg = msgFactory.getMessage("AddPortMapping");
try {
String localIP = InetAddress.getLocalHost().getHostAddress();
msg.setInputParameter("NewRemoteHost", "")
.setInputParameter("NewExternalPort", port)
.setInputParameter("NewProtocol", "TCP")
.setInputParameter("NewInternalPort", port)
.setInputParameter("NewInternalClient", localIP)
.setInputParameter("NewEnabled", "1")
.setInputParameter("NewPortMappingDescription", "Remote Object " + this.getClass().getName() + " " + this.hashCode())
.setInputParameter("NewLeaseDuration", "0");
msg.service();
portOpened = true;
UnicastObjectShutdownHook hook = new UnicastObjectShutdownHook(this);
Runtime.getRuntime().addShutdownHook(hook);
} catch (Exception ex) {
if (fail) {
throw new RemoteException("Error occured during port mapping", ex);
}
}
}
}
/**
* Closes the port on the UPNP router
*/
public void closePort() {
if (wanConnDevice != null && portOpened) {
net.sbbi.upnp.services.UPNPService wanIPSrv = wanConnDevice.getService("urn:schemas-upnp-org:service:WANIPConnection:1");
if (wanIPSrv != null) {
UPNPMessageFactory msgFactory = UPNPMessageFactory.getNewInstance(wanIPSrv);
ActionMessage msg = msgFactory.getMessage("DeletePortMapping");
try {
msg.setInputParameter("NewRemoteHost", "")
.setInputParameter("NewExternalPort", port)
.setInputParameter("NewProtocol", "TCP");
msg.service();
portOpened = false;
} catch (Exception ex) {
// silently ignoring
}
}
}
}
private final void discoverDevice() throws RemoteException {
synchronized (DISCOVERY_PROCESS) {
if (!discoveryProcessCall) {
discoveryProcessCall = true;
UPNPRootDevice rootIGDDevice = null;
String failStr = System.getProperty("net.sbbi.upnp.remote.failWhenNoDeviceFound");
boolean fail = false;
if (failStr != null && failStr.equalsIgnoreCase("true"))
fail = true;
UPNPRootDevice[] devices = null;
try {
String timeout = System.getProperty("net.sbbi.upnp.remote.discoveryTimeout");
if (timeout != null) {
devices = Discovery.discover(Integer.parseInt(timeout), "urn:schemas-upnp-org:device:InternetGatewayDevice:1");
} else {
devices = Discovery.discover("urn:schemas-upnp-org:device:InternetGatewayDevice:1");
}
} catch (IOException ex) {
throw new RemoteException("IOException occured during devices discovery", ex);
}
if (devices == null && fail)
throw new IllegalStateException("No UPNP IGD (urn:schemas-upnp-org:device:InternetGatewayDevice:1) available, unable to create object");
if (devices != null && devices.length > 1) {
String deviceUDN = System.getProperty("net.sbbi.upnp.remote.deviceUDN");
if (deviceUDN == null) {
String UDNs = "";
for (int i = 0; i < devices.length; i++) {
UPNPRootDevice dv = devices[i];
UDNs += dv.getUDN();
if (i < devices.length) {
UDNs += ", ";
}
}
throw new RemoteException("Found multiple IDG UPNP devices UDN's :" + UDNs + ", " +
"please set the net.sbbi.upnp.remote.deviceUDN system " +
"property with one of the following identifier to define " +
"which UPNP device need to be used with remote objects");
}
StringBuffer foundUDN = new StringBuffer();
for (int i = 0; i < devices.length; i++) {
UPNPRootDevice dv = devices[i];
if (dv.getUDN().equals(deviceUDN)) {
rootIGDDevice = dv;
break;
}
foundUDN.append(dv.getUDN());
if (i < devices.length)
foundUDN.append(", ");
}
if (rootIGDDevice == null)
throw new RemoteException("No UPNP device matching UDN :" + deviceUDN + ", found UDN(s) are :" + foundUDN);
} else if (devices != null) {
rootIGDDevice = devices[0];
}
if (rootIGDDevice != null) {
wanConnDevice = rootIGDDevice.getChildDevice("urn:schemas-upnp-org:device:WANConnectionDevice:1");
if (wanConnDevice == null && fail)
throw new RemoteException("Your UPNP device does not implements urn:schemas-upnp-org:device:WANConnectionDevice:1 required specs for NAT transversal");
}
}
}
}
private class UnicastObjectShutdownHook extends Thread {
private final UnicastRemoteObject object;
private UnicastObjectShutdownHook(UnicastRemoteObject object) {
this.object = object;
}
@Override
public void run() {
object.closePort();
}
}
}