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 }