1 /* 2 * Copyright (c) 2002, 2008, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 27 package javax.management.remote; 28 29 30 import com.sun.jmx.remote.util.ClassLogger; 31 import com.sun.jmx.remote.util.EnvHelp; 32 33 import java.io.Serializable; 34 import java.net.InetAddress; 35 import java.net.MalformedURLException; 36 import java.net.UnknownHostException; 37 import java.util.BitSet; 38 import java.util.StringTokenizer; 39 40 /** 41 * <p>The address of a JMX API connector server. Instances of this class 42 * are immutable.</p> 43 * 44 * <p>The address is an <em>Abstract Service URL</em> for SLP, as 45 * defined in RFC 2609 and amended by RFC 3111. It must look like 46 * this:</p> 47 * 48 * <blockquote> 49 * 50 * <code>service:jmx:<em>protocol</em>:<em>sap</em></code> 51 * 52 * </blockquote> 53 * 54 * <p>Here, <code><em>protocol</em></code> is the transport 55 * protocol to be used to connect to the connector server. It is 56 * a string of one or more ASCII characters, each of which is a 57 * letter, a digit, or one of the characters <code>+</code> or 58 * <code>-</code>. The first character must be a letter. 59 * Uppercase letters are converted into lowercase ones.</p> 60 * 61 * <p><code><em>sap</em></code> is the address at which the connector 62 * server is found. This address uses a subset of the syntax defined 63 * by RFC 2609 for IP-based protocols. It is a subset because the 64 * <code>user@host</code> syntax is not supported.</p> 65 * 66 * <p>The other syntaxes defined by RFC 2609 are not currently 67 * supported by this class.</p> 68 * 69 * <p>The supported syntax is:</p> 70 * 71 * <blockquote> 72 * 73 * <code>//<em>[host[</em>:<em>port]][url-path]</em></code> 74 * 75 * </blockquote> 76 * 77 * <p>Square brackets <code>[]</code> indicate optional parts of 78 * the address. Not all protocols will recognize all optional 79 * parts.</p> 80 * 81 * <p>The <code><em>host</em></code> is a host name, an IPv4 numeric 82 * host address, or an IPv6 numeric address enclosed in square 83 * brackets.</p> 84 * 85 * <p>The <code><em>port</em></code> is a decimal port number. 0 86 * means a default or anonymous port, depending on the protocol.</p> 87 * 88 * <p>The <code><em>host</em></code> and <code><em>port</em></code> 89 * can be omitted. The <code><em>port</em></code> cannot be supplied 90 * without a <code><em>host</em></code>.</p> 91 * 92 * <p>The <code><em>url-path</em></code>, if any, begins with a slash 93 * (<code>/</code>) or a semicolon (<code>;</code>) and continues to 94 * the end of the address. It can contain attributes using the 95 * semicolon syntax specified in RFC 2609. Those attributes are not 96 * parsed by this class and incorrect attribute syntax is not 97 * detected.</p> 98 * 99 * <p>Although it is legal according to RFC 2609 to have a 100 * <code><em>url-path</em></code> that begins with a semicolon, not 101 * all implementations of SLP allow it, so it is recommended to avoid 102 * that syntax.</p> 103 * 104 * <p>Case is not significant in the initial 105 * <code>service:jmx:<em>protocol</em></code> string or in the host 106 * part of the address. Depending on the protocol, case can be 107 * significant in the <code><em>url-path</em></code>.</p> 108 * 109 * @see <a 110 * href="http://www.ietf.org/rfc/rfc2609.txt">RFC 2609, 111 * "Service Templates and <code>Service:</code> Schemes"</a> 112 * @see <a 113 * href="http://www.ietf.org/rfc/rfc3111.txt">RFC 3111, 114 * "Service Location Protocol Modifications for IPv6"</a> 115 * 116 * @since 1.5 117 */ 118 public class JMXServiceURL implements Serializable { 119 120 private static final long serialVersionUID = 8173364409860779292L; 121 122 /** 123 * <p>Constructs a <code>JMXServiceURL</code> by parsing a Service URL 124 * string.</p> 125 * 126 * @param serviceURL the URL string to be parsed. 127 * 128 * @exception NullPointerException if <code>serviceURL</code> is 129 * null. 130 * 131 * @exception MalformedURLException if <code>serviceURL</code> 132 * does not conform to the syntax for an Abstract Service URL or 133 * if it is not a valid name for a JMX Remote API service. A 134 * <code>JMXServiceURL</code> must begin with the string 135 * <code>"service:jmx:"</code> (case-insensitive). It must not 136 * contain any characters that are not printable ASCII characters. 137 */ 138 public JMXServiceURL(String serviceURL) throws MalformedURLException { 139 final int serviceURLLength = serviceURL.length(); 140 141 /* Check that there are no non-ASCII characters in the URL, 142 following RFC 2609. */ 143 for (int i = 0; i < serviceURLLength; i++) { 144 char c = serviceURL.charAt(i); 145 if (c < 32 || c >= 127) { 146 throw new MalformedURLException("Service URL contains " + 147 "non-ASCII character 0x" + 148 Integer.toHexString(c)); 149 } 150 } 151 152 // Parse the required prefix 153 final String requiredPrefix = "service:jmx:"; 154 final int requiredPrefixLength = requiredPrefix.length(); 155 if (!serviceURL.regionMatches(true, // ignore case 156 0, // serviceURL offset 157 requiredPrefix, 158 0, // requiredPrefix offset 159 requiredPrefixLength)) { 160 throw new MalformedURLException("Service URL must start with " + 161 requiredPrefix); 162 } 163 164 // Parse the protocol name 165 final int protoStart = requiredPrefixLength; 166 final int protoEnd = indexOf(serviceURL, ':', protoStart); 167 this.protocol = 168 serviceURL.substring(protoStart, protoEnd).toLowerCase(); 169 170 if (!serviceURL.regionMatches(protoEnd, "://", 0, 3)) { 171 throw new MalformedURLException("Missing \"://\" after " + 172 "protocol name"); 173 } 174 175 // Parse the host name 176 final int hostStart = protoEnd + 3; 177 final int hostEnd; 178 if (hostStart < serviceURLLength 179 && serviceURL.charAt(hostStart) == '[') { 180 hostEnd = serviceURL.indexOf(']', hostStart) + 1; 181 if (hostEnd == 0) 182 throw new MalformedURLException("Bad host name: [ without ]"); 183 this.host = serviceURL.substring(hostStart + 1, hostEnd - 1); 184 if (!isNumericIPv6Address(this.host)) { 185 throw new MalformedURLException("Address inside [...] must " + 186 "be numeric IPv6 address"); 187 } 188 } else { 189 hostEnd = 190 indexOfFirstNotInSet(serviceURL, hostNameBitSet, hostStart); 191 this.host = serviceURL.substring(hostStart, hostEnd); 192 } 193 194 // Parse the port number 195 final int portEnd; 196 if (hostEnd < serviceURLLength && serviceURL.charAt(hostEnd) == ':') { 197 if (this.host.length() == 0) { 198 throw new MalformedURLException("Cannot give port number " + 199 "without host name"); 200 } 201 final int portStart = hostEnd + 1; 202 portEnd = 203 indexOfFirstNotInSet(serviceURL, numericBitSet, portStart); 204 final String portString = serviceURL.substring(portStart, portEnd); 205 try { 206 this.port = Integer.parseInt(portString); 207 } catch (NumberFormatException e) { 208 throw new MalformedURLException("Bad port number: \"" + 209 portString + "\": " + e); 210 } 211 } else { 212 portEnd = hostEnd; 213 this.port = 0; 214 } 215 216 // Parse the URL path 217 final int urlPathStart = portEnd; 218 if (urlPathStart < serviceURLLength) 219 this.urlPath = serviceURL.substring(urlPathStart); 220 else 221 this.urlPath = ""; 222 223 validate(); 224 } 225 226 /** 227 * <p>Constructs a <code>JMXServiceURL</code> with the given protocol, 228 * host, and port. This constructor is equivalent to 229 * {@link #JMXServiceURL(String, String, int, String) 230 * JMXServiceURL(protocol, host, port, null)}.</p> 231 * 232 * @param protocol the protocol part of the URL. If null, defaults 233 * to <code>jmxmp</code>. 234 * 235 * @param host the host part of the URL. If null, defaults to the 236 * local host name, as determined by 237 * <code>InetAddress.getLocalHost().getHostName()</code>. If it 238 * is a numeric IPv6 address, it can optionally be enclosed in 239 * square brackets <code>[]</code>. 240 * 241 * @param port the port part of the URL. 242 * 243 * @exception MalformedURLException if one of the parts is 244 * syntactically incorrect, or if <code>host</code> is null and it 245 * is not possible to find the local host name, or if 246 * <code>port</code> is negative. 247 */ 248 public JMXServiceURL(String protocol, String host, int port) 249 throws MalformedURLException { 250 this(protocol, host, port, null); 251 } 252 253 /** 254 * <p>Constructs a <code>JMXServiceURL</code> with the given parts. 255 * 256 * @param protocol the protocol part of the URL. If null, defaults 257 * to <code>jmxmp</code>. 258 * 259 * @param host the host part of the URL. If null, defaults to the 260 * local host name, as determined by 261 * <code>InetAddress.getLocalHost().getHostName()</code>. If it 262 * is a numeric IPv6 address, it can optionally be enclosed in 263 * square brackets <code>[]</code>. 264 * 265 * @param port the port part of the URL. 266 * 267 * @param urlPath the URL path part of the URL. If null, defaults to 268 * the empty string. 269 * 270 * @exception MalformedURLException if one of the parts is 271 * syntactically incorrect, or if <code>host</code> is null and it 272 * is not possible to find the local host name, or if 273 * <code>port</code> is negative. 274 */ 275 public JMXServiceURL(String protocol, String host, int port, 276 String urlPath) 277 throws MalformedURLException { 278 if (protocol == null) 279 protocol = "jmxmp"; 280 281 if (host == null) { 282 InetAddress local; 283 try { 284 local = InetAddress.getLocalHost(); 285 } catch (UnknownHostException e) { 286 throw new MalformedURLException("Local host name unknown: " + 287 e); 288 } 289 290 host = local.getHostName(); 291 292 /* We might have a hostname that violates DNS naming 293 rules, for example that contains an `_'. While we 294 could be strict and throw an exception, this is rather 295 user-hostile. Instead we use its numerical IP address. 296 We can only reasonably do this for the host==null case. 297 If we're given an explicit host name that is illegal we 298 have to reject it. (Bug 5057532.) */ 299 try { 300 validateHost(host); 301 } catch (MalformedURLException e) { 302 if (logger.fineOn()) { 303 logger.fine("JMXServiceURL", 304 "Replacing illegal local host name " + 305 host + " with numeric IP address " + 306 "(see RFC 1034)", e); 307 } 308 host = local.getHostAddress(); 309 /* Use the numeric address, which could be either IPv4 310 or IPv6. validateHost will accept either. */ 311 } 312 } 313 314 if (host.startsWith("[")) { 315 if (!host.endsWith("]")) { 316 throw new MalformedURLException("Host starts with [ but " + 317 "does not end with ]"); 318 } 319 host = host.substring(1, host.length() - 1); 320 if (!isNumericIPv6Address(host)) { 321 throw new MalformedURLException("Address inside [...] must " + 322 "be numeric IPv6 address"); 323 } 324 if (host.startsWith("[")) 325 throw new MalformedURLException("More than one [[...]]"); 326 } 327 328 this.protocol = protocol.toLowerCase(); 329 this.host = host; 330 this.port = port; 331 332 if (urlPath == null) 333 urlPath = ""; 334 this.urlPath = urlPath; 335 336 validate(); 337 } 338 339 private void validate() throws MalformedURLException { 340 341 // Check protocol 342 343 final int protoEnd = indexOfFirstNotInSet(protocol, protocolBitSet, 0); 344 if (protoEnd == 0 || protoEnd < protocol.length() 345 || !alphaBitSet.get(protocol.charAt(0))) { 346 throw new MalformedURLException("Missing or invalid protocol " + 347 "name: \"" + protocol + "\""); 348 } 349 350 // Check host 351 352 validateHost(); 353 354 // Check port 355 356 if (port < 0) 357 throw new MalformedURLException("Bad port: " + port); 358 359 // Check URL path 360 361 if (urlPath.length() > 0) { 362 if (!urlPath.startsWith("/") && !urlPath.startsWith(";")) 363 throw new MalformedURLException("Bad URL path: " + urlPath); 364 } 365 } 366 367 private void validateHost() throws MalformedURLException { 368 if (host.length() == 0) { 369 if (port != 0) { 370 throw new MalformedURLException("Cannot give port number " + 371 "without host name"); 372 } 373 return; 374 } 375 376 validateHost(host); 377 } 378 379 private static void validateHost(String h) 380 throws MalformedURLException { 381 382 if (isNumericIPv6Address(h)) { 383 /* We assume J2SE >= 1.4 here. Otherwise you can't 384 use the address anyway. We can't call 385 InetAddress.getByName without checking for a 386 numeric IPv6 address, because we mustn't try to do 387 a DNS lookup in case the address is not actually 388 numeric. */ 389 try { 390 InetAddress.getByName(h); 391 } catch (Exception e) { 392 /* We should really catch UnknownHostException 393 here, but a bug in JDK 1.4 causes it to throw 394 ArrayIndexOutOfBoundsException, e.g. if the 395 string is ":". */ 396 MalformedURLException bad = 397 new MalformedURLException("Bad IPv6 address: " + h); 398 EnvHelp.initCause(bad, e); 399 throw bad; 400 } 401 } else { 402 /* Tiny state machine to check valid host name. This 403 checks the hostname grammar from RFC 1034 (DNS), 404 page 11. A hostname is a dot-separated list of one 405 or more labels, where each label consists of 406 letters, numbers, or hyphens. A label cannot begin 407 or end with a hyphen. Empty hostnames are not 408 allowed. Note that numeric IPv4 addresses are a 409 special case of this grammar. 410 411 The state is entirely captured by the last 412 character seen, with a virtual `.' preceding the 413 name. We represent any alphanumeric character by 414 `a'. 415 416 We need a special hack to check, as required by the 417 RFC 2609 (SLP) grammar, that the last component of 418 the hostname begins with a letter. Respecting the 419 intent of the RFC, we only do this if there is more 420 than one component. If your local hostname begins 421 with a digit, we don't reject it. */ 422 final int hostLen = h.length(); 423 char lastc = '.'; 424 boolean sawDot = false; 425 char componentStart = 0; 426 427 loop: 428 for (int i = 0; i < hostLen; i++) { 429 char c = h.charAt(i); 430 boolean isAlphaNumeric = alphaNumericBitSet.get(c); 431 if (lastc == '.') 432 componentStart = c; 433 if (isAlphaNumeric) 434 lastc = 'a'; 435 else if (c == '-') { 436 if (lastc == '.') 437 break; // will throw exception 438 lastc = '-'; 439 } else if (c == '.') { 440 sawDot = true; 441 if (lastc != 'a') 442 break; // will throw exception 443 lastc = '.'; 444 } else { 445 lastc = '.'; // will throw exception 446 break; 447 } 448 } 449 450 try { 451 if (lastc != 'a') 452 throw randomException; 453 if (sawDot && !alphaBitSet.get(componentStart)) { 454 /* Must be a numeric IPv4 address. In addition to 455 the explicitly-thrown exceptions, we can get 456 NoSuchElementException from the calls to 457 tok.nextToken and NumberFormatException from 458 the call to Integer.parseInt. Using exceptions 459 for control flow this way is a bit evil but it 460 does simplify things enormously. */ 461 StringTokenizer tok = new StringTokenizer(h, ".", true); 462 for (int i = 0; i < 4; i++) { 463 String ns = tok.nextToken(); 464 int n = Integer.parseInt(ns); 465 if (n < 0 || n > 255) 466 throw randomException; 467 if (i < 3 && !tok.nextToken().equals(".")) 468 throw randomException; 469 } 470 if (tok.hasMoreTokens()) 471 throw randomException; 472 } 473 } catch (Exception e) { 474 throw new MalformedURLException("Bad host: \"" + h + "\""); 475 } 476 } 477 } 478 479 private static final Exception randomException = new Exception(); 480 481 482 /** 483 * <p>The protocol part of the Service URL. 484 * 485 * @return the protocol part of the Service URL. This is never null. 486 */ 487 public String getProtocol() { 488 return protocol; 489 } 490 491 /** 492 * <p>The host part of the Service URL. If the Service URL was 493 * constructed with the constructor that takes a URL string 494 * parameter, the result is the substring specifying the host in 495 * that URL. If the Service URL was constructed with a 496 * constructor that takes a separate host parameter, the result is 497 * the string that was specified. If that string was null, the 498 * result is 499 * <code>InetAddress.getLocalHost().getHostName()</code>.</p> 500 * 501 * <p>In either case, if the host was specified using the 502 * <code>[...]</code> syntax for numeric IPv6 addresses, the 503 * square brackets are not included in the return value here.</p> 504 * 505 * @return the host part of the Service URL. This is never null. 506 */ 507 public String getHost() { 508 return host; 509 } 510 511 /** 512 * <p>The port of the Service URL. If no port was 513 * specified, the returned value is 0.</p> 514 * 515 * @return the port of the Service URL, or 0 if none. 516 */ 517 public int getPort() { 518 return port; 519 } 520 521 /** 522 * <p>The URL Path part of the Service URL. This is an empty 523 * string, or a string beginning with a slash (<code>/</code>), or 524 * a string beginning with a semicolon (<code>;</code>). 525 * 526 * @return the URL Path part of the Service URL. This is never 527 * null. 528 */ 529 public String getURLPath() { 530 return urlPath; 531 } 532 533 /** 534 * <p>The string representation of this Service URL. If the value 535 * returned by this method is supplied to the 536 * <code>JMXServiceURL</code> constructor, the resultant object is 537 * equal to this one.</p> 538 * 539 * <p>The <code><em>host</em></code> part of the returned string 540 * is the value returned by {@link #getHost()}. If that value 541 * specifies a numeric IPv6 address, it is surrounded by square 542 * brackets <code>[]</code>.</p> 543 * 544 * <p>The <code><em>port</em></code> part of the returned string 545 * is the value returned by {@link #getPort()} in its shortest 546 * decimal form. If the value is zero, it is omitted.</p> 547 * 548 * @return the string representation of this Service URL. 549 */ 550 public String toString() { 551 /* We don't bother synchronizing the access to toString. At worst, 552 n threads will independently compute and store the same value. */ 553 if (toString != null) 554 return toString; 555 StringBuilder buf = new StringBuilder("service:jmx:"); 556 buf.append(getProtocol()).append("://"); 557 final String getHost = getHost(); 558 if (isNumericIPv6Address(getHost)) 559 buf.append('[').append(getHost).append(']'); 560 else 561 buf.append(getHost); 562 final int getPort = getPort(); 563 if (getPort != 0) 564 buf.append(':').append(getPort); 565 buf.append(getURLPath()); 566 toString = buf.toString(); 567 return toString; 568 } 569 570 /** 571 * <p>Indicates whether some other object is equal to this one. 572 * This method returns true if and only if <code>obj</code> is an 573 * instance of <code>JMXServiceURL</code> whose {@link 574 * #getProtocol()}, {@link #getHost()}, {@link #getPort()}, and 575 * {@link #getURLPath()} methods return the same values as for 576 * this object. The values for {@link #getProtocol()} and {@link 577 * #getHost()} can differ in case without affecting equality. 578 * 579 * @param obj the reference object with which to compare. 580 * 581 * @return <code>true</code> if this object is the same as the 582 * <code>obj</code> argument; <code>false</code> otherwise. 583 */ 584 public boolean equals(Object obj) { 585 if (!(obj instanceof JMXServiceURL)) 586 return false; 587 JMXServiceURL u = (JMXServiceURL) obj; 588 return 589 (u.getProtocol().equalsIgnoreCase(getProtocol()) && 590 u.getHost().equalsIgnoreCase(getHost()) && 591 u.getPort() == getPort() && 592 u.getURLPath().equals(getURLPath())); 593 } 594 595 public int hashCode() { 596 return toString().hashCode(); 597 } 598 599 /* True if this string, assumed to be a valid argument to 600 * InetAddress.getByName, is a numeric IPv6 address. 601 */ 602 private static boolean isNumericIPv6Address(String s) { 603 // address contains colon if and only if it's a numeric IPv6 address 604 return (s.indexOf(':') >= 0); 605 } 606 607 // like String.indexOf but returns string length not -1 if not present 608 private static int indexOf(String s, char c, int fromIndex) { 609 int index = s.indexOf(c, fromIndex); 610 if (index < 0) 611 return s.length(); 612 else 613 return index; 614 } 615 616 private static int indexOfFirstNotInSet(String s, BitSet set, 617 int fromIndex) { 618 final int slen = s.length(); 619 int i = fromIndex; 620 while (true) { 621 if (i >= slen) 622 break; 623 char c = s.charAt(i); 624 if (c >= 128) 625 break; // not ASCII 626 if (!set.get(c)) 627 break; 628 i++; 629 } 630 return i; 631 } 632 633 private final static BitSet alphaBitSet = new BitSet(128); 634 private final static BitSet numericBitSet = new BitSet(128); 635 private final static BitSet alphaNumericBitSet = new BitSet(128); 636 private final static BitSet protocolBitSet = new BitSet(128); 637 private final static BitSet hostNameBitSet = new BitSet(128); 638 static { 639 /* J2SE 1.4 adds lots of handy methods to BitSet that would 640 allow us to simplify here, e.g. by not writing loops, but 641 we want to work on J2SE 1.3 too. */ 642 643 for (char c = '0'; c <= '9'; c++) 644 numericBitSet.set(c); 645 646 for (char c = 'A'; c <= 'Z'; c++) 647 alphaBitSet.set(c); 648 for (char c = 'a'; c <= 'z'; c++) 649 alphaBitSet.set(c); 650 651 alphaNumericBitSet.or(alphaBitSet); 652 alphaNumericBitSet.or(numericBitSet); 653 654 protocolBitSet.or(alphaNumericBitSet); 655 protocolBitSet.set('+'); 656 protocolBitSet.set('-'); 657 658 hostNameBitSet.or(alphaNumericBitSet); 659 hostNameBitSet.set('-'); 660 hostNameBitSet.set('.'); 661 } 662 663 /** 664 * The value returned by {@link #getProtocol()}. 665 */ 666 private final String protocol; 667 668 /** 669 * The value returned by {@link #getHost()}. 670 */ 671 private final String host; 672 673 /** 674 * The value returned by {@link #getPort()}. 675 */ 676 private final int port; 677 678 /** 679 * The value returned by {@link #getURLPath()}. 680 */ 681 private final String urlPath; 682 683 /** 684 * Cached result of {@link #toString()}. 685 */ 686 private transient String toString; 687 688 private static final ClassLogger logger = 689 new ClassLogger("javax.management.remote.misc", "JMXServiceURL"); 690 }