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