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