SocketPermission.java revision a9c6c9013b08934867f71b69a90efce0c1b66918
1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package java.net; 19 20import java.io.IOException; 21import java.io.ObjectInputStream; 22import java.io.ObjectOutputStream; 23import java.io.Serializable; 24import java.security.Permission; 25import java.security.PermissionCollection; 26 27/** 28 * Regulates the access to network operations available through sockets through 29 * permissions. A permission consists of a target (a host), and an associated 30 * action list. The target should identify the host by either indicating the 31 * (possibly wildcarded (eg. {@code .company.com})) DNS style name of the host 32 * or its IP address in standard {@code nn.nn.nn.nn} ("dot") notation. The 33 * action list can be made up of one or more of the following actions separated 34 * by a comma: 35 * <dl> 36 * <dt>connect</dt> 37 * <dd>requests permission to connect to the host</dd> 38 * <dt>listen</dt> 39 * <dd>requests permission to listen for connections from the host</dd> 40 * <dt>accept</dt> 41 * <dd>requests permission to accept connections from the host</dd> 42 * <dt>resolve</dt> 43 * <dd>requests permission to resolve the hostname</dd> 44 * </dl> 45 * Note that {@code resolve} is implied when any (or none) of the others are 46 * present. 47 * <p> 48 * Access to a particular port can be requested by appending a colon and a 49 * single digit to the name (eg. {@code .company.com:7000}). A range of port 50 * numbers can also be specified, by appending a pattern of the form 51 * <i>LOW-HIGH</i> where <i>LOW</i> and <i>HIGH</i> are valid port numbers. If 52 * either <i>LOW</i> or <i>HIGH</i> is omitted it is equivalent to entering the 53 * lowest or highest possible value respectively. For example: 54 * 55 * <pre> 56 * {@code SocketPermission("www.company.com:7000-", "connect,accept")} 57 * </pre> 58 * 59 * represents the permission to connect to and accept connections from {@code 60 * www.company.com} on ports in the range {@code 7000} to {@code 65535}. 61 */ 62public final class SocketPermission extends Permission implements Serializable { 63 64 private static final long serialVersionUID = -7204263841984476862L; 65 66 // Bit masks for each of the possible actions 67 static final int SP_CONNECT = 1; 68 69 static final int SP_LISTEN = 2; 70 71 static final int SP_ACCEPT = 4; 72 73 static final int SP_RESOLVE = 8; 74 75 // list of actions permitted for socket permission in order, indexed by mask 76 // value 77 @SuppressWarnings("nls") 78 private static final String[] actionNames = { "", "connect", "listen", "", 79 "accept", "", "", "", "resolve" }; 80 81 // If a wildcard is present store the information 82 private transient boolean isPartialWild; 83 84 private transient boolean isWild; 85 86 // The highest port number 87 private static final int HIGHEST_PORT = 65535; 88 89 // The lowest port number 90 private static final int LOWEST_PORT = 0; 91 92 transient String hostName; // Host name as returned by InetAddress 93 94 transient String ipString; // IP address as returned by InetAddress 95 96 transient boolean resolved; // IP address has been resolved 97 98 // the port range; 99 transient int portMin = LOWEST_PORT; 100 101 transient int portMax = HIGHEST_PORT; 102 103 private String actions; // List of all actions allowed by this permission 104 105 transient int actionsMask = SP_RESOLVE; 106 107 /** 108 * Constructs a new {@code SocketPermission} instance. The hostname can be a 109 * DNS name, an individual hostname, an IP address or the empty string which 110 * implies {@code localhost}. The port or port range is optional. 111 * <p> 112 * The action list is a comma-separated list which can consists of the 113 * possible operations {@code "connect"}, {@code "listen"}, {@code "accept"} 114 * , and {@code "resolve"}. They are case-insensitive and can be put 115 * together in any order. {@code "resolve"} is implied per default. 116 * 117 * @param host 118 * the hostname this permission is valid for. 119 * @param action 120 * the action string of this permission. 121 */ 122 public SocketPermission(String host, String action) { 123 super(host.isEmpty() ? "localhost" : host); 124 hostName = getHostString(host); 125 if (action == null) { 126 throw new NullPointerException(); 127 } 128 if (action.isEmpty()) { 129 throw new IllegalArgumentException(); 130 } 131 132 setActions(action); 133 actions = toCanonicalActionString(action); 134 // Use host since we are only checking for port presence 135 parsePort(host, hostName); 136 } 137 138 /** 139 * Compares the argument {@code o} to this instance and returns {@code true} 140 * if they represent the same permission using a class specific comparison. 141 * 142 * @param other 143 * the object to compare with this {@code SocketPermission} 144 * instance. 145 * @return {@code true} if they represent the same permission, {@code false} 146 * otherwise. 147 * @see #hashCode 148 */ 149 @Override 150 public boolean equals(Object other) { 151 if (this == other) { 152 return true; 153 } 154 if (other == null || this.getClass() != other.getClass()) { 155 return false; 156 } 157 SocketPermission sp = (SocketPermission) other; 158 if (!hostName.equalsIgnoreCase(sp.hostName)) { 159 if (getIPString(true) == null || !ipString.equalsIgnoreCase(sp.getIPString(true))) { 160 return false; 161 } 162 } 163 if (this.actionsMask != SP_RESOLVE) { 164 if (this.portMin != sp.portMin) { 165 return false; 166 } 167 if (this.portMax != sp.portMax) { 168 return false; 169 } 170 } 171 return this.actionsMask == sp.actionsMask; 172 } 173 174 /** 175 * Returns the hash value for this {@code SocketPermission} instance. Any 176 * two objects which returns {@code true} when passed to {@code equals()} 177 * must return the same value as a result of this method. 178 * 179 * @return the hashcode value for this instance. 180 * @see #equals 181 */ 182 @Override 183 public int hashCode() { 184 return hostName.hashCode() ^ actionsMask ^ portMin ^ portMax; 185 } 186 187 /** 188 * Gets a comma-separated list of all actions allowed by this permission. If 189 * more than one action is returned they follow this order: {@code connect}, 190 * {@code listen}, {@code accept}, {@code resolve}. 191 * 192 * @return the comma-separated action list. 193 */ 194 @Override 195 public String getActions() { 196 return actions; 197 } 198 199 /** 200 * Stores the actions for this permission as a bit field. 201 * 202 * @param actions 203 * java.lang.String the action list 204 */ 205 private void setActions(String actions) throws IllegalArgumentException { 206 if (actions.isEmpty()) { 207 return; 208 } 209 boolean parsing = true; 210 String action; 211 StringBuilder sb = new StringBuilder(); 212 int pos = 0, length = actions.length(); 213 while (parsing) { 214 char c; 215 sb.setLength(0); 216 while (pos < length && (c = actions.charAt(pos++)) != ',') { 217 sb.append(c); 218 } 219 if (pos == length) { 220 parsing = false; 221 } 222 action = sb.toString().trim().toLowerCase(); 223 if (action.equals(actionNames[SP_CONNECT])) { 224 actionsMask |= SP_CONNECT; 225 } else if (action.equals(actionNames[SP_LISTEN])) { 226 actionsMask |= SP_LISTEN; 227 } else if (action.equals(actionNames[SP_ACCEPT])) { 228 actionsMask |= SP_ACCEPT; 229 } else if (action.equals(actionNames[SP_RESOLVE])) { 230 // do nothing 231 } else { 232 throw new IllegalArgumentException("Invalid action: " + action); 233 } 234 } 235 } 236 237 /** 238 * Checks whether this {@code SocketPermission} instance allows all actions 239 * which are allowed by the given permission object {@code p}. All argument 240 * permission actions, hosts and ports must be implied by this permission 241 * instance in order to return {@code true}. This permission may imply 242 * additional actions not present in the argument permission. 243 * 244 * @param p 245 * the socket permission which has to be implied by this 246 * instance. 247 * @return {@code true} if this permission instance implies all permissions 248 * represented by {@code p}, {@code false} otherwise. 249 */ 250 @Override 251 public boolean implies(Permission p) { 252 SocketPermission sp; 253 try { 254 sp = (SocketPermission) p; 255 } catch (ClassCastException e) { 256 return false; 257 } 258 259 // tests if the action list of p is the subset of the one of the 260 // receiver 261 if (sp == null || (actionsMask & sp.actionsMask) != sp.actionsMask) { 262 return false; 263 } 264 265 // only check the port range if the action string of the current object 266 // is not "resolve" 267 if (!p.getActions().equals("resolve")) { 268 if ((sp.portMin < this.portMin) || (sp.portMax > this.portMax)) { 269 return false; 270 } 271 } 272 273 // Verify the host is valid 274 return checkHost(sp); 275 } 276 277 /** 278 * Creates a new {@code PermissionCollection} to store {@code 279 * SocketPermission} objects. 280 * 281 * @return the new permission collection. 282 */ 283 @Override 284 public PermissionCollection newPermissionCollection() { 285 return new SocketPermissionCollection(); 286 } 287 288 /** 289 * Parse the port, including the minPort, maxPort 290 * @param hostPort the host[:port] one 291 * @param host the host name we just get 292 * @throws IllegalArgumentException If the port is not a positive number or minPort 293 * is not less than or equal maxPort 294 */ 295 private void parsePort(String hostPort, String host) throws IllegalArgumentException { 296 String port = hostPort.substring(host.length()); 297 String emptyString = ""; 298 299 if (emptyString.equals(port)) { 300 // Not specified 301 portMin = 80; 302 portMax = 80; 303 return; 304 } 305 306 if (":*".equals(port)) { 307 // The port range should be 0-65535 308 portMin = 0; 309 portMax = 65535; 310 return; 311 } 312 313 // Omit ':' 314 port = port.substring(1); 315 int negIdx = port.indexOf('-'); 316 String strPortMin = emptyString; 317 String strPortMax = emptyString; 318 if (-1 == negIdx) { 319 // No neg mark, only one number 320 strPortMin = port; 321 strPortMax = port; 322 } else { 323 strPortMin = port.substring(0, negIdx); 324 strPortMax = port.substring(negIdx + 1); 325 if (emptyString.equals(strPortMin)) { 326 strPortMin = "0"; 327 } 328 if (emptyString.equals(strPortMax)) { 329 strPortMax = "65535"; 330 } 331 } 332 try { 333 portMin = Integer.valueOf(strPortMin).intValue(); 334 portMax = Integer.valueOf(strPortMax).intValue(); 335 336 if (portMin > portMax) { 337 throw new IllegalArgumentException("MinPort is greater than MaxPort: " + port); 338 } 339 } catch (NumberFormatException e) { 340 throw new IllegalArgumentException("Invalid port number: " + port); 341 } 342 } 343 344 /** 345 * Creates a canonical action list. 346 * 347 * @param action 348 * java.lang.String 349 * 350 * @return java.lang.String 351 */ 352 private String toCanonicalActionString(String action) { 353 if (action == null || action.isEmpty() || actionsMask == SP_RESOLVE) { 354 return actionNames[SP_RESOLVE]; // If none specified return the 355 } 356 // implied action resolve 357 StringBuilder sb = new StringBuilder(); 358 if ((actionsMask & SP_CONNECT) == SP_CONNECT) { 359 sb.append(','); 360 sb.append(actionNames[SP_CONNECT]); 361 } 362 if ((actionsMask & SP_LISTEN) == SP_LISTEN) { 363 sb.append(','); 364 sb.append(actionNames[SP_LISTEN]); 365 } 366 if ((actionsMask & SP_ACCEPT) == SP_ACCEPT) { 367 sb.append(','); 368 sb.append(actionNames[SP_ACCEPT]); 369 } 370 sb.append(','); 371 sb.append(actionNames[SP_RESOLVE]);// Resolve is always implied 372 // Don't copy the first ','. 373 return actions = sb.substring(1, sb.length()); 374 } 375 376 private String getIPString(boolean isCheck) { 377 if (!resolved) { 378 try { 379 ipString = InetAddress.getHostNameInternal(hostName, isCheck); 380 } catch (UnknownHostException e) { 381 // ignore 382 } 383 resolved = true; 384 } 385 return ipString; 386 } 387 388 /** 389 * Get the host part from the host[:port] one. The host should be 390 * 391 * <pre> 392 * host = (hostname | IPv4address | IPv6reference | IPv6 in full uncompressed form) 393 * </pre> 394 * 395 * The wildcard "*" may be included once in a DNS name host specification. 396 * If it is included, it must be in the leftmost position 397 * 398 * @param host 399 * the {@code host[:port]} string. 400 * @return the host name. 401 * @throws IllegalArgumentException 402 * if the host is invalid. 403 */ 404 private String getHostString(String host) throws IllegalArgumentException { 405 host = host.trim(); 406 int idx = -1; 407 idx = host.indexOf(':'); 408 isPartialWild = (host.length() > 0 && host.charAt(0) == '*'); 409 if (isPartialWild) { 410 resolved = true; 411 isWild = (host.length() == 1); 412 if (isWild) { 413 return host; 414 } 415 if (idx > -1) { 416 host = host.substring(0, idx); 417 } 418 return host.toLowerCase(); 419 } 420 421 int lastIdx = host.lastIndexOf(':'); 422 423 if (idx == lastIdx) { 424 if (-1 != idx) { 425 // only one colon, should be port 426 host = host.substring(0, idx); 427 } 428 return host.toLowerCase(); 429 } 430 // maybe IPv6 431 boolean isFirstBracket = (host.charAt(0) == '['); 432 if (!isFirstBracket) { 433 // No bracket, should be in full form 434 int colonNum = 0; 435 for (int i = 0; i < host.length(); ++i) { 436 if (host.charAt(i) == ':') { 437 colonNum++; 438 } 439 } 440 // Get rid of the colon before port 441 if (8 == colonNum) { 442 host = host.substring(0, lastIdx); 443 } 444 if (isIP6AddressInFullForm(host)) { 445 return host.toLowerCase(); 446 } 447 throw new IllegalArgumentException("Invalid port number: " + host); 448 } 449 // forward bracket found 450 int bbracketIdx = host.indexOf(']'); 451 if (-1 == bbracketIdx) { 452 // no back bracket found, wrong 453 throw new IllegalArgumentException("Invalid port number: " + host); 454 } 455 host = host.substring(0, bbracketIdx + 1); 456 if (isValidIP6Address(host)) { 457 return host.toLowerCase(); 458 } 459 throw new IllegalArgumentException("Invalid port number: " + host); 460 } 461 462 private static boolean isValidHexChar(char c) { 463 return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); 464 } 465 466 private static boolean isValidIP4Word(String word) { 467 char c; 468 if (word.length() < 1 || word.length() > 3) { 469 return false; 470 } 471 for (int i = 0; i < word.length(); i++) { 472 c = word.charAt(i); 473 if (!(c >= '0' && c <= '9')) { 474 return false; 475 } 476 } 477 if (Integer.parseInt(word) > 255) { 478 return false; 479 } 480 return true; 481 } 482 483 private static boolean isIP6AddressInFullForm(String ipAddress) { 484 if (isValidIP6Address(ipAddress)) { 485 int doubleColonIndex = ipAddress.indexOf("::"); 486 if (doubleColonIndex >= 0) { 487 // Simplified form which contains :: 488 return false; 489 } 490 return true; 491 } 492 return false; 493 } 494 495 private static boolean isValidIP6Address(String ipAddress) { 496 int length = ipAddress.length(); 497 boolean doubleColon = false; 498 int numberOfColons = 0; 499 int numberOfPeriods = 0; 500 int numberOfPercent = 0; 501 String word = ""; 502 char c = 0; 503 char prevChar = 0; 504 int offset = 0; // offset for [] IP addresses 505 506 if (length < 2) { 507 return false; 508 } 509 510 for (int i = 0; i < length; i++) { 511 prevChar = c; 512 c = ipAddress.charAt(i); 513 switch (c) { 514 515 // case for an open bracket [x:x:x:...x] 516 case '[': 517 if (i != 0) { 518 return false; // must be first character 519 } 520 if (ipAddress.charAt(length - 1) != ']') { 521 return false; // must have a close ] 522 } 523 offset = 1; 524 if (length < 4) { 525 return false; 526 } 527 break; 528 529 // case for a closed bracket at end of IP [x:x:x:...x] 530 case ']': 531 if (i != length - 1) { 532 return false; // must be last character 533 } 534 if (ipAddress.charAt(0) != '[') { 535 return false; // must have a open [ 536 } 537 break; 538 539 // case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d 540 case '.': 541 numberOfPeriods++; 542 if (numberOfPeriods > 3) { 543 return false; 544 } 545 if (!isValidIP4Word(word)) { 546 return false; 547 } 548 if (numberOfColons != 6 && !doubleColon) { 549 return false; 550 } 551 // a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an 552 // IPv4 ending, otherwise 7 :'s is bad 553 if (numberOfColons == 7 && ipAddress.charAt(0 + offset) != ':' 554 && ipAddress.charAt(1 + offset) != ':') { 555 return false; 556 } 557 word = ""; 558 break; 559 560 case ':': 561 numberOfColons++; 562 if (numberOfColons > 7) { 563 return false; 564 } 565 if (numberOfPeriods > 0) { 566 return false; 567 } 568 if (prevChar == ':') { 569 if (doubleColon) { 570 return false; 571 } 572 doubleColon = true; 573 } 574 word = ""; 575 break; 576 case '%': 577 if (numberOfColons == 0) { 578 return false; 579 } 580 numberOfPercent++; 581 582 // validate that the stuff after the % is valid 583 if ((i + 1) >= length) { 584 // in this case the percent is there but no number is 585 // available 586 return false; 587 } 588 try { 589 Integer.parseInt(ipAddress.substring(i + 1)); 590 } catch (NumberFormatException e) { 591 // right now we just support an integer after the % so if 592 // this is not 593 // what is there then return 594 return false; 595 } 596 break; 597 598 default: 599 if (numberOfPercent == 0) { 600 if (word.length() > 3) { 601 return false; 602 } 603 if (!isValidHexChar(c)) { 604 return false; 605 } 606 } 607 word += c; 608 } 609 } 610 611 // Check if we have an IPv4 ending 612 if (numberOfPeriods > 0) { 613 if (numberOfPeriods != 3 || !isValidIP4Word(word)) { 614 return false; 615 } 616 } else { 617 // If we're at then end and we haven't had 7 colons then there is a 618 // problem unless we encountered a doubleColon 619 if (numberOfColons != 7 && !doubleColon) { 620 return false; 621 } 622 623 // If we have an empty word at the end, it means we ended in either 624 // a : or a . 625 // If we did not end in :: then this is invalid 626 if (numberOfPercent == 0) { 627 if (word == "" && ipAddress.charAt(length - 1 - offset) == ':' 628 && ipAddress.charAt(length - 2 - offset) != ':') { 629 return false; 630 } 631 } 632 } 633 634 return true; 635 } 636 637 /** 638 * Determines whether or not this permission could refer to the same host as 639 * sp. 640 */ 641 boolean checkHost(SocketPermission sp) { 642 if (isPartialWild) { 643 if (isWild) { 644 return true; // Match on any host 645 } 646 int length = hostName.length() - 1; 647 return sp.hostName.regionMatches(sp.hostName.length() - length, 648 hostName, 1, length); 649 } 650 // The ipString may not be the same, some hosts resolve to 651 // multiple ips 652 return (getIPString(false) != null && ipString.equals(sp.getIPString(false))) 653 || hostName.equals(sp.hostName); 654 } 655 656 private void writeObject(ObjectOutputStream stream) throws IOException { 657 stream.defaultWriteObject(); 658 } 659 660 private void readObject(ObjectInputStream stream) throws IOException, 661 ClassNotFoundException { 662 stream.defaultReadObject(); 663 // Initialize locals 664 isPartialWild = false; 665 isWild = false; 666 portMin = LOWEST_PORT; 667 portMax = HIGHEST_PORT; 668 actionsMask = SP_RESOLVE; 669 hostName = getHostString(getName()); 670 parsePort(getName(), hostName); 671 setActions(actions); 672 } 673} 674