/* * 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(); } } }