View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    * 
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   * 
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package net.jini.core.discovery;
19  
20  import org.apache.river.discovery.Discovery;
21  import org.apache.river.discovery.DiscoveryConstraints;
22  import org.apache.river.discovery.DiscoveryProtocolVersion;
23  import org.apache.river.discovery.UnicastResponse;
24  import org.apache.river.discovery.UnicastSocketTimeout;
25  import org.apache.river.discovery.MultiIPDiscovery;
26  import java.io.IOException;
27  import java.io.InvalidObjectException;
28  import java.io.ObjectInputStream;
29  import java.io.ObjectOutputStream;
30  import java.io.ObjectStreamField;
31  import java.io.Serializable;
32  import java.net.MalformedURLException;
33  import java.net.Socket;
34  import java.net.URI;
35  import java.net.URISyntaxException;
36  import java.security.AccessController;
37  import java.security.PrivilegedAction;
38  import java.util.Collection;
39  import net.jini.core.constraint.InvocationConstraints;
40  import net.jini.core.lookup.ServiceRegistrar;
41  import org.apache.river.api.io.AtomicSerial;
42  import org.apache.river.api.io.AtomicSerial.GetArg;
43  import org.apache.river.api.net.Uri;
44  
45  /**
46   * LookupLocator supports unicast discovery, using either Discovery V1 or V2 or 
47   * https.
48   * 
49   * Version 1 of the unicast discovery protocol is deprecated.
50   * Discovery V2 is used by default, unless otherwise set by Constraints.
51   * <p>
52   * It's main purpose now is to contain a url used for unicast discovery of 
53   * a {@link ServiceRegistrar}, it is now
54   * immutable, since River 2.2.1, this may break overriding classes.
55   * <p>
56   * <code>"jini"</code> Unicast Discovery may be performed using any Discovery 
57   * V1 or V2 provider and depending on routing and firewall rules,
58   * may be used to contact a {@link ServiceRegistrar}.  
59   * The <code>"https"</code> scheme based Unicast Discovery is neither
60   * Discovery V1 or V2 compliant, as firewall rules and proxy servers 
61   * that allow https communications are likely to prevent the handshake 
62   * required to select a <code>"jini"</code> Discovery provider.
63   * <p>
64   * LookupLocator is used as a parameter in LookupLocatorDiscovery constructors.  
65   * LookupLocatorDiscovery has methods to perform Discovery using either 
66   * version 1 or 2 with constraints.
67   * ConstrainableLookupLocator is a subclass which uses discovery V1 or V2
68   * and enables the use of constraints.
69   *
70   * @since 1.0
71   * <br><code>see net.jini.discovery.LookupLocatorDiscovery</code>
72   * @see net.jini.discovery.ConstrainableLookupLocator
73   */
74  @AtomicSerial
75  public class LookupLocator implements Serializable {
76      private static final long serialVersionUID = 1448769379829432795L;
77      private static final ObjectStreamField[] serialPersistentFields = 
78      { 
79          /** @serialField The name of the host at which to perform discovery. */
80          new ObjectStreamField("host", String.class),
81  	/** @serialField The port number on the host at which to perform discovery. */
82  	new ObjectStreamField("port", Integer.TYPE)
83      };
84      /**
85       * The port for both unicast and multicast boot requests.
86       */
87      private static final short DISCOVERY_PORT = 4160;
88      
89      private static final short HTTPS_DISCOVERY_PORT = 443;
90      
91      /**
92       * The name of the host at which to perform discovery.
93       *
94       * @serial
95       */
96      protected final String host;
97      /**
98       * The port number on the host at which to perform discovery.
99       *
100      * @serial
101      */
102     protected final int port;
103     
104     /**
105      * Either <code>"jini"</code> or <code>"https"</code>.
106      * 
107      * @serial
108      */
109     private final String scheme;
110     
111     /**
112      * The timeout after which we give up waiting for a response from
113      * the lookup service.
114      */
115     private static final int defaultTimeout =
116 	AccessController.doPrivileged(new PrivilegedAction<Integer>() {
117             @Override
118 	    public Integer run() {
119 		Integer timeout = Integer.valueOf(60 * 1000);
120 		try {
121 		    Integer val = Integer.getInteger(
122 				    "net.jini.discovery.timeout",
123 				    timeout);
124 		    return (val.intValue() < 0 ? timeout : val);
125 		} catch (SecurityException e) {
126 		    return timeout;
127 		}
128 	    }
129 	}).intValue();
130 
131     /**
132      * Construct a new <code>LookupLocator</code> object, set up to perform
133      * discovery to the given URL.  The <code>host</code> and <code>port</code>
134      * fields will be populated with the <i>host</i> and <i>port</i>
135      * components of the input URL.  No host name resolution is attempted.
136      * <p>
137      * The syntax of the URL must be that of a <i>hierarchical</i> 
138      * {@link java.net.URI} with a <i>server-based naming authority</i>.
139      * Requirements for the components are as follows:
140      * <ul>
141      * <li> A <i>scheme</i> of <code>"jini"</code> or <code>"https"</code> must be present.
142      * <li> A <i>server-based naming authority</i> must be
143      * present; <i>user-info</i> must not be present. The <i>port</i>, if
144      * present, must be between 1 and 65535 (both included). If no port is
145      * specified, a default value of <code>4160</code> is used for <code>"jini"</code>
146      * and a default value of <code>443</code> is used for <code>"https"</code>.
147      * <li> A <i>path</i> if present, must be
148      * <code>"/"</code>; <i>path segment</i>s must not be present.
149      * <li> There must not be any other components present.
150      * </ul>
151      * <p>
152      * The four allowed forms of the URL are thus:
153      * <ul>
154      * <li> <code>scheme://</code><i>host</i>
155      * <li> <code>scheme://</code><i>host</i><code>/</code>
156      * <li> <code>scheme://</code><i>host</i><code>:</code><i>port</i>
157      * <li>
158      * <code>scheme://</code><i>host</i><code>:</code><i>port</i><code>/</code>
159      * </ul>
160      * @param url the URL to use
161      * @throws MalformedURLException <code>url</code> could not be parsed
162      * @throws NullPointerException if <code>url</code> is <code>null</code>
163      */
164     public LookupLocator(String url) throws MalformedURLException {
165 	this(parseURI(url));
166     }
167     
168     /**
169      * Only this constructor doesn't check invariants.
170      * @param uri 
171      */
172     private LookupLocator(URI uri){
173 	super();
174         scheme = uri.getScheme();
175         host = uri.getHost();
176         port = uri.getPort();
177     }
178 
179     /**
180      * Check invariants before super() is called.
181      * @param host
182      * @param port
183      * @return 
184      */
185     private static URI parseURI(String scheme, String host, int port){
186 	if (host == null) {
187             throw new NullPointerException("null host");
188         }
189         StringBuilder sb = new StringBuilder();
190         sb.append(scheme).append("://").append(host);
191         if (port != -1) { //URI compliance -1 is converted to discoveryPort.
192             sb.append(":").append(port);
193         }
194         try {
195             return parseURI(sb.toString());
196         } catch (MalformedURLException ex) {
197             throw new IllegalArgumentException("host cannot be parsed", ex);
198         }
199     }
200     
201     public LookupLocator(GetArg arg) throws IOException{
202 	this(parseURI(getScheme(arg), arg.get("host", null, String.class), arg.get("port", 0)));
203     }
204     
205     private static String getScheme(GetArg arg) throws IOException{
206         String scheme = "jini";
207         try {
208             scheme = arg.get("scheme", scheme, String.class);
209         } catch (IllegalArgumentException e){
210             // Ignore, just means the field doesn't exist in serial form.
211         }
212         return scheme;
213     }
214 
215     /**
216      * Construct a new <code>LookupLocator</code> object, set to perform unicast
217      * discovery to the input <code>host</code> and <code>port</code> with the 
218      * default <code>"jini"</code> scheme.  The
219      * <code>host</code> and <code>port</code> fields will be populated with the
220      * <code>host</code> and <code>port</code> arguments.  No host name
221      * resolution is attempted.
222      * <p>The <code>host</code>
223      * argument must meet any one of the following syntactical requirements:
224      * <ul>
225      * <li>A host as required by a <i>server-based naming authority</i> in
226      * section 3.2.2 of <a href="http://www.ietf.org/rfc/rfc2396.txt">
227      * <i>RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax</i></a>
228      * <li>A literal IPv6 address as defined by
229      * <a href="http://www.ietf.org/rfc/rfc2732.txt">
230      * <i>RFC 2732: Format for Literal IPv6 Addresses in URL's</i></a>
231      * <li>A literal IPv6 address as defined by
232      * <a href="http://www.ietf.org/rfc/rfc3513.txt">
233      * <i>RFC 3513: Internet Protocol Version 6 (IPv6) Addressing Architecture
234      * </i></a>
235      * </ul>
236      *
237      * @param host the name of the host to contact
238      * @param port the number of the port to connect to
239      * @throws IllegalArgumentException if <code>port</code> is not between
240      * 1 and 65535 (both included) or if <code>host</code> cannot be parsed.
241      * @throws NullPointerException if <code>host</code> is <code>null</code>
242      */
243     public LookupLocator(String host, int port) {
244         this(parseURI("jini", host, port));
245         }
246 
247     /**
248      * Check invariants before super() is called.
249      */
250     private static URI parseURI(String url) throws MalformedURLException {
251         if (url == null) {
252             throw new NullPointerException("url is null");
253         }
254         URI uri = null;
255         try {
256             uri = Uri.uriToURI(Uri.parseAndCreate(url));
257         } catch (URISyntaxException e) {
258             MalformedURLException mue
259                     = new MalformedURLException("URI parsing failure: " + url);
260             mue.initCause(e);
261             throw mue;
262         }
263 	if (!uri.isAbsolute()) throw new MalformedURLException("no scheme specified: " + url);
264 	if (uri.isOpaque()) throw new MalformedURLException("not a hierarchical url: " + url);
265         String scheme = uri.getScheme().toLowerCase();
266 	if (!("jini".equals(scheme)|| "https".equals(scheme))) 
267             throw new MalformedURLException("Invalid URL scheme: " + scheme);
268 
269         String uriPath = uri.getPath();
270         if ((uriPath.length() != 0) && (!uriPath.equals("/"))) {
271             throw new MalformedURLException(
272                     "URL path contains path segments: " + url);
273         }
274 	if (uri.getQuery() != null) throw new MalformedURLException("invalid character, '?', in URL: " + url);
275 	if (uri.getFragment() != null) throw new MalformedURLException("invalid character, '#', in URL: " + url);
276         if (uri.getUserInfo() != null) throw new MalformedURLException("invalid character, '@', in URL host: " + url);
277         if ((uri.getHost()) == null) {
278             // authority component does not exist - not a hierarchical URL
279             throw new MalformedURLException(
280                     "Not a hierarchical URL: " + url);
281         }
282         int port = uri.getPort();
283         if (port == -1) {
284             port = "https".equals(scheme)? HTTPS_DISCOVERY_PORT : DISCOVERY_PORT;
285             try {
286                 uri = new URI(
287                         uri.getScheme(),
288                         uri.getRawUserInfo(),
289                         uri.getHost(), 
290                         port, 
291                         uri.getRawPath(),
292                         uri.getRawQuery(),
293                         uri.getRawFragment()
294                 );
295             } catch (URISyntaxException e) {
296                 MalformedURLException mue
297                         = new MalformedURLException("recreation of URI with discovery port failed");
298                 mue.initCause(e);
299                 throw mue;
300             }
301         }
302 
303         if ((uri.getPort() <= 0) || (uri.getPort() >= 65536)) {
304             throw new MalformedURLException("port number out of range: " + url);
305         }
306         return uri;
307     }
308     
309     /**
310      * Returns the scheme used by this LookupLocator, either traditional
311      * "jini" or "https".
312      * 
313      * @return scheme string.
314      */
315     public String scheme() {
316         return scheme == null ? "jini" : scheme;
317     }
318 
319     /**
320      * Returns the name of the host that this instance should contact.
321      * <code>LookupLocator</code> implements this method to return
322      * the <code>host</code> field.
323      *
324      * @return a String representing the host value
325      */
326     public String getHost() {
327 	return host;
328     }
329 
330     /**
331      * Returns the number of the port to which this instance should connect.
332      * <code>LookupLocator</code> implements this method to return the
333      * <code>port</code> field.
334      *
335      * @return an int representing the port value
336      */
337     public int getPort() {
338 	return port;
339     }
340 
341     /**
342      * Perform unicast discovery and return the ServiceRegistrar
343      * object for the given lookup service.  Unicast discovery is
344      * performed anew each time this method is called.
345      * <code>LookupLocator</code> implements this method to simply invoke
346      * {@link #getRegistrar(int)} with a timeout value, which is determined
347      * by the value of the <code>net.jini.discovery.timeout</code> system
348      * property.  If the property is set, is not negative, and can be parsed as
349      * an <code>Integer</code>, the value of the property is used as the timeout
350      * value. Otherwise, a default value of <code>60</code> seconds is assumed.
351      *
352      * @return the ServiceRegistrar for the lookup service denoted by
353      * this LookupLocator object
354      * @throws IOException an error occurred during discovery
355      * @throws ClassNotFoundException if a class required to unmarshal the
356      * <code>ServiceRegistrar</code> proxy cannot be found
357      */
358     public ServiceRegistrar getRegistrar()
359 	throws IOException, ClassNotFoundException
360     {
361 	return getRegistrar(defaultTimeout);
362     }
363 
364     /**
365      * Perform unicast discovery and return the ServiceRegistrar
366      * object for the given lookup service, with the given discovery timeout.
367      * Unicast discovery is performed anew each time this method is called.
368      * <code>LookupLocator</code> implements this method to use the values
369      * of the <code>host</code> and <code>port</code> field in determining
370      * the host and port to connect to.
371      *
372      * <p>
373      * If a connection can be established to start unicast discovery
374      * but the remote end fails to respond within the given time
375      * limit, an exception is thrown.
376      *
377      * @param timeout the maximum time to wait for a response, in
378      * milliseconds.  A value of <code>0</code> specifies an infinite timeout.
379      * @return the ServiceRegistrar for the lookup service denoted by
380      * this LookupLocator object
381      * @throws IOException an error occurred during discovery
382      * @throws ClassNotFoundException if a class required to unmarshal the
383      * <code>ServiceRegistrar</code> proxy cannot be found
384      * @throws IllegalArgumentException if <code>timeout</code> is negative
385      */
386     public ServiceRegistrar getRegistrar(int timeout)
387 	throws IOException, ClassNotFoundException
388     {
389 	return getRegistrar(
390                 new InvocationConstraints(
391                         new UnicastSocketTimeout(timeout), 
392                         DiscoveryProtocolVersion.TWO 
393                 )
394         );
395     }
396 
397     /**
398      * Perform unicast discovery and return the ServiceRegistrar
399      * object for the given lookup service, with the given constraints.
400      * 
401      * Unicast discovery is performed anew each time this method is called.
402      * <code>LookupLocator</code> implements this method to use the values
403      * of the <code>host</code> and <code>port</code> field in determining
404      * the host and port to connect to.
405      * @param constraints discovery constraints
406      * @return lookup service proxy
407      * @throws IOException an error occurred during discovery
408      * @throws net.jini.io.UnsupportedConstraintException if the
409      * discovery-related constraints contain conflicts, or otherwise cannot be
410      * processed
411      * @throws ClassNotFoundException if a class required to unmarshal the
412      * <code>ServiceRegistrar</code> proxy cannot be found
413      */
414     protected final ServiceRegistrar getRegistrar(InvocationConstraints constraints)
415             throws IOException, ClassNotFoundException {
416         return getRegistrar(constraints, null);
417     }
418     
419     /**
420      * Perform unicast discovery and return the ServiceRegistrar
421      * object for the given lookup service, with the given constraints
422      * and context.
423      * 
424      * Note the context may include {@link net.jini.core.constraint.MethodConstraints}
425      * for {@link net.jini.loader.ProxyCodebaseSpi}.
426      * 
427      * Unicast discovery is performed anew each time this method is called.
428      * <code>LookupLocator</code> implements this method to use the values
429      * of the <code>host</code> and <code>port</code> field in determining
430      * the host and port to connect to.
431      * @param constraints discovery constraints
432      * @param context the stream context {@link net.jini.io.ObjectStreamContext#getObjectStreamContext() }
433      * @return lookup service proxy
434      * @throws IOException an error occurred during discovery
435      * @throws net.jini.io.UnsupportedConstraintException if the
436      * discovery-related constraints contain conflicts, or otherwise cannot be
437      * processed
438      * @throws ClassNotFoundException if a class required to unmarshal the
439      * <code>ServiceRegistrar</code> proxy cannot be found
440      */
441     protected final ServiceRegistrar getRegistrar(
442 					    InvocationConstraints constraints,
443 					    final Collection context)
444 	throws IOException, ClassNotFoundException 
445     {
446 	UnicastResponse resp = new MultiIPDiscovery() {
447             @Override
448             protected UnicastResponse performDiscovery(Discovery disco,
449                     DiscoveryConstraints dc,
450                     Socket s)
451                     throws IOException, ClassNotFoundException {
452                 return disco.doUnicastDiscovery(
453                         s, dc.getUnfulfilledConstraints(), null, null, context);
454             }
455         }.getResponse(scheme(), host, port, constraints);
456         return resp.getRegistrar();
457     }
458     
459     /**
460      * Return the string form of this LookupLocator, as a URL of scheme
461      * <code>"jini"</code> or <code>"https"</code>.
462      */
463     public String toString() {
464         StringBuilder sb = new StringBuilder();
465         sb.append(scheme()).append("://").append(getHost0(host)).append(":").append(port).append("/");
466         return sb.toString();
467     }
468 
469     /**
470      * Two locators are equal if they have the same <code>host</code> and
471      * <code>port</code> fields. The case of the <code>host</code> is ignored.
472      * Alternative forms of the same IPv6 addresses for the <code>host</code>
473      * value are treated as being unequal.
474      */
475     public boolean equals(Object o) {
476 	if (o == this) {
477 	    return true;
478 	}
479 	if (o instanceof LookupLocator) {
480 	    LookupLocator oo = (LookupLocator) o;
481 	    return port == oo.port && host.equalsIgnoreCase(oo.host);
482 	}
483 	return false;
484     }
485 
486     /**
487      * Returns a hash code value calculated from the <code>host</code> and
488      * <code>port</code> field values.
489      */
490     public int hashCode() {
491 	return host.toLowerCase().hashCode() ^ port;
492     }
493     
494     // Checks if the host is an RFC 3513 IPv6 literal and converts it into
495     // RFC 2732 form.
496     private static String getHost0(String host) {
497 	if ((host.indexOf(':') >= 0) && (host.charAt(0) != '[')) {
498 	    // This is a 3513 form IPv6 literal
499 	    return '[' + host + ']';
500 	} else {
501 	    return host;
502 	}
503     }
504     
505     private void writeObject(ObjectOutputStream out) throws IOException {
506 	out.defaultWriteObject();
507     }
508     
509     /**
510      * Added to allow deserialisation of broken serial compatibility in 2.2.0
511      * 
512      * Invariants are not protected against finalizer and circular reference
513      * attacks with standard de-serialization, ensure trust is established
514      * prior to obtaining an instance of LookupLocator from a Remote interface
515      * or ObjectInputStream.
516      * 
517      * @serial
518      * @param oin
519      * @throws IOException
520      * @throws ClassNotFoundException 
521      */
522     private void readObject(ObjectInputStream oin) 
523 	    throws IOException, ClassNotFoundException{
524         oin.defaultReadObject();
525 	try {
526 	    parseURI(scheme(), host, port);
527 	} catch (NullPointerException ex){
528 	    InvalidObjectException e = new InvalidObjectException(
529 		    "Invariants not satisfied during deserialization");
530 	    e.initCause(ex);
531 	    throw e;
532 	} catch (IllegalArgumentException ex){
533 	    InvalidObjectException e = new InvalidObjectException(
534 		    "Invariants not satisfied during deserialization");
535 	    e.initCause(ex);
536 	    throw e;
537     }
538 }
539 }