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 }