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