/*
* ============================================================================
* 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.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
*/
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 UnicastRemoteObject object;
private UnicastObjectShutdownHook( UnicastRemoteObject object ) {
this.object = object;
}
public void run() {
object.closePort();
}
}
}