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
18/**
19* @author Alexey V. Varlamov
20* @version $Revision$
21*/
22
23package org.apache.harmony.security;
24
25import java.io.IOException;
26import java.io.Reader;
27import java.io.StreamTokenizer;
28import java.util.Collection;
29import java.util.HashSet;
30import java.util.List;
31
32import org.apache.harmony.security.internal.nls.Messages;
33
34/**
35 * This is a basic high-level tokenizer of policy files. It takes in a stream,
36 * analyzes data read from it and returns a set of structured tokens. <br>
37 * This implementation recognizes text files, consisting of clauses with the
38 * following syntax:
39 *
40 * <pre>
41 *
42 *     keystore &quot;some_keystore_url&quot;, &quot;keystore_type&quot;;
43 *
44 * </pre>
45 * <pre>
46 *
47 *     grant [SignedBy &quot;signer_names&quot;] [, CodeBase &quot;URL&quot;]
48 *      [, Principal [principal_class_name] &quot;principal_name&quot;]
49 *      [, Principal [principal_class_name] &quot;principal_name&quot;] ... {
50 *      permission permission_class_name [ &quot;target_name&quot; ] [, &quot;action&quot;]
51 *      [, SignedBy &quot;signer_names&quot;];
52 *      permission ...
53 *      };
54 *
55 * </pre>
56 *
57 * For semantical details of this format, see the
58 * {@link org.apache.harmony.security.fortress.DefaultPolicy default policy description}.
59 * <br>
60 * Keywords are case-insensitive in contrast to quoted string literals.
61 * Comma-separation rule is quite forgiving, most commas may be just omitted.
62 * Whitespaces, line- and block comments are ignored. Symbol-level tokenization
63 * is delegated to java.io.StreamTokenizer. <br>
64 * <br>
65 * This implementation is effectively thread-safe, as it has no field references
66 * to data being processed (that is, passes all the data as method parameters).
67 *
68 * @see org.apache.harmony.security.fortress.DefaultPolicyParser
69 */
70public class DefaultPolicyScanner {
71
72    /**
73     * Specific exception class to signal policy file syntax error.
74     *
75     */
76    public static class InvalidFormatException extends Exception {
77
78        /**
79         * @serial
80         */
81        private static final long serialVersionUID = 5789786270390222184L;
82
83        /**
84         * Constructor with detailed message parameter.
85         */
86        public InvalidFormatException(String arg0) {
87            super(arg0);
88        }
89    }
90
91    /**
92     * Configures passed tokenizer accordingly to supported syntax.
93     */
94    protected StreamTokenizer configure(StreamTokenizer st) {
95        st.slashSlashComments(true);
96        st.slashStarComments(true);
97        st.wordChars('_', '_');
98        st.wordChars('$', '$');
99        return st;
100    }
101
102    /**
103     * Performs the main parsing loop. Starts with creating and configuring a
104     * StreamTokenizer instance; then tries to recognize <i>keystore </i> or
105     * <i>grant </i> keyword. When found, invokes read method corresponding to
106     * the clause and collects result to the passed collection.
107     *
108     * @param r
109     *            policy stream reader
110     * @param grantEntries
111     *            a collection to accumulate parsed GrantEntries
112     * @param keystoreEntries
113     *            a collection to accumulate parsed KeystoreEntries
114     * @throws IOException
115     *             if stream reading failed
116     * @throws InvalidFormatException
117     *             if unexpected or unknown token encountered
118     */
119    public void scanStream(Reader r, Collection<GrantEntry> grantEntries,
120            List<KeystoreEntry> keystoreEntries) throws IOException,
121            InvalidFormatException {
122        StreamTokenizer st = configure(new StreamTokenizer(r));
123        //main parsing loop
124        parsing: while (true) {
125            switch (st.nextToken()) {
126            case StreamTokenizer.TT_EOF: //we've done the job
127                break parsing;
128
129            case StreamTokenizer.TT_WORD:
130                if (Util.equalsIgnoreCase("keystore", st.sval)) { //$NON-NLS-1$
131                    keystoreEntries.add(readKeystoreEntry(st));
132                } else if (Util.equalsIgnoreCase("grant", st.sval)) { //$NON-NLS-1$
133                    grantEntries.add(readGrantEntry(st));
134                } else {
135                    handleUnexpectedToken(st, Messages.getString("security.89")); //$NON-NLS-1$
136                }
137                break;
138
139            case ';': //just delimiter of entries
140                break;
141
142            default:
143                handleUnexpectedToken(st);
144                break;
145            }
146        }
147    }
148
149    /**
150     * Tries to read <i>keystore </i> clause fields. The expected syntax is
151     *
152     * <pre>
153     *
154     *     &quot;some_keystore_url&quot;[, &quot;keystore_type&quot;];
155     *
156     * </pre>
157     *
158     * @return successfully parsed KeystoreEntry
159     * @throws IOException
160     *             if stream reading failed
161     * @throws InvalidFormatException
162     *             if unexpected or unknown token encountered
163     */
164    protected KeystoreEntry readKeystoreEntry(StreamTokenizer st)
165            throws IOException, InvalidFormatException {
166        KeystoreEntry ke = new KeystoreEntry();
167        if (st.nextToken() == '"') {
168            ke.url = st.sval;
169            if ((st.nextToken() == '"')
170                    || ((st.ttype == ',') && (st.nextToken() == '"'))) {
171                ke.type = st.sval;
172            } else { // handle token in the main loop
173                st.pushBack();
174            }
175        } else {
176            handleUnexpectedToken(st, Messages.getString("security.8A")); //$NON-NLS-1$
177        }
178        return ke;
179    }
180
181    /**
182     * Tries to read <i>grant </i> clause. <br>
183     * First, it reads <i>codebase </i>, <i>signedby </i>, <i>principal </i>
184     * entries till the '{' (opening curly brace) symbol. Then it calls
185     * readPermissionEntries() method to read the permissions of this clause.
186     * <br>
187     * Principal entries (if any) are read by invoking readPrincipalEntry()
188     * method, obtained PrincipalEntries are accumulated. <br>
189     * The expected syntax is
190     *
191     * <pre>
192     *
193     *     [ [codebase &quot;url&quot;] | [signedby &quot;name1,...,nameN&quot;] |
194     *          principal ...] ]* { ... }
195     *
196     * </pre>
197     *
198     * @return successfully parsed GrantEntry
199     * @throws IOException
200     *             if stream reading failed
201     * @throws InvalidFormatException
202     *             if unexpected or unknown token encountered
203     */
204    protected GrantEntry readGrantEntry(StreamTokenizer st) throws IOException,
205            InvalidFormatException {
206        GrantEntry ge = new GrantEntry();
207        parsing: while (true) {
208            switch (st.nextToken()) {
209
210            case StreamTokenizer.TT_WORD:
211                if (Util.equalsIgnoreCase("signedby", st.sval)) { //$NON-NLS-1$
212                    if (st.nextToken() == '"') {
213                        ge.signers = st.sval;
214                    } else {
215                        handleUnexpectedToken(st, Messages.getString("security.8B")); //$NON-NLS-1$
216                    }
217                } else if (Util.equalsIgnoreCase("codebase", st.sval)) { //$NON-NLS-1$
218                    if (st.nextToken() == '"') {
219                        ge.codebase = st.sval;
220                    } else {
221                        handleUnexpectedToken(st, Messages.getString("security.8C")); //$NON-NLS-1$
222                    }
223                } else if (Util.equalsIgnoreCase("principal", st.sval)) { //$NON-NLS-1$
224                    ge.addPrincipal(readPrincipalEntry(st));
225                } else {
226                    handleUnexpectedToken(st);
227                }
228                break;
229
230            case ',': //just delimiter of entries
231                break;
232
233            case '{':
234                ge.permissions = readPermissionEntries(st);
235                break parsing;
236
237            default: // handle token in the main loop
238                st.pushBack();
239                break parsing;
240            }
241        }
242
243        return ge;
244    }
245
246    /**
247     * Tries to read <i>Principal </i> entry fields. The expected syntax is
248     *
249     * <pre>
250     *
251     *     [ principal_class_name ] &quot;principal_name&quot;
252     *
253     * </pre>
254     *
255     * Both class and name may be wildcards, wildcard names should not
256     * surrounded by quotes.
257     *
258     * @return successfully parsed PrincipalEntry
259     * @throws IOException
260     *             if stream reading failed
261     * @throws InvalidFormatException
262     *             if unexpected or unknown token encountered
263     */
264    protected PrincipalEntry readPrincipalEntry(StreamTokenizer st)
265            throws IOException, InvalidFormatException {
266        PrincipalEntry pe = new PrincipalEntry();
267        if (st.nextToken() == StreamTokenizer.TT_WORD) {
268            pe.klass = st.sval;
269            st.nextToken();
270        } else if (st.ttype == '*') {
271            pe.klass = PrincipalEntry.WILDCARD;
272            st.nextToken();
273        }
274        if (st.ttype == '"') {
275            pe.name = st.sval;
276        } else if (st.ttype == '*') {
277            pe.name = PrincipalEntry.WILDCARD;
278        } else {
279            handleUnexpectedToken(st, Messages.getString("security.8D")); //$NON-NLS-1$
280        }
281        return pe;
282    }
283
284    /**
285     * Tries to read a list of <i>permission </i> entries. The expected syntax
286     * is
287     *
288     * <pre>
289     *
290     *     permission permission_class_name
291     *          [ &quot;target_name&quot; ] [, &quot;action_list&quot;]
292     *          [, signedby &quot;name1,name2,...&quot;];
293     *
294     * </pre>
295     *
296     * List is terminated by '}' (closing curly brace) symbol.
297     *
298     * @return collection of successfully parsed PermissionEntries
299     * @throws IOException
300     *             if stream reading failed
301     * @throws InvalidFormatException
302     *             if unexpected or unknown token encountered
303     */
304    protected Collection<PermissionEntry> readPermissionEntries(
305            StreamTokenizer st) throws IOException, InvalidFormatException {
306        Collection<PermissionEntry> permissions = new HashSet<PermissionEntry>();
307        parsing: while (true) {
308            switch (st.nextToken()) {
309
310            case StreamTokenizer.TT_WORD:
311                if (Util.equalsIgnoreCase("permission", st.sval)) { //$NON-NLS-1$
312                    PermissionEntry pe = new PermissionEntry();
313                    if (st.nextToken() == StreamTokenizer.TT_WORD) {
314                        pe.klass = st.sval;
315                        if (st.nextToken() == '"') {
316                            pe.name = st.sval;
317                            st.nextToken();
318                        }
319                        if (st.ttype == ',') {
320                            st.nextToken();
321                        }
322                        if (st.ttype == '"') {
323                            pe.actions = st.sval;
324                            if (st.nextToken() == ',') {
325                                st.nextToken();
326                            }
327                        }
328                        if (st.ttype == StreamTokenizer.TT_WORD
329                                && Util.equalsIgnoreCase("signedby", st.sval)) { //$NON-NLS-1$
330                            if (st.nextToken() == '"') {
331                                pe.signers = st.sval;
332                            } else {
333                                handleUnexpectedToken(st);
334                            }
335                        } else { // handle token in the next iteration
336                            st.pushBack();
337                        }
338                        permissions.add(pe);
339                        continue parsing;
340                    }
341                }
342                handleUnexpectedToken(st, Messages.getString("security.8E")); //$NON-NLS-1$
343                break;
344
345            case ';': //just delimiter of entries
346                break;
347
348            case '}': //end of list
349                break parsing;
350
351            default: // invalid token
352                handleUnexpectedToken(st);
353                break;
354            }
355        }
356
357        return permissions;
358    }
359
360    /**
361     * Formats a detailed description of tokenizer status: current token,
362     * current line number, etc.
363     */
364    protected String composeStatus(StreamTokenizer st) {
365        return st.toString();
366    }
367
368    /**
369     * Throws InvalidFormatException with detailed diagnostics.
370     *
371     * @param st
372     *            a tokenizer holding the erroneous token
373     * @param message
374     *            a user-friendly comment, probably explaining expected syntax.
375     *            Should not be <code>null</code>- use the overloaded
376     *            single-parameter method instead.
377     */
378    protected final void handleUnexpectedToken(StreamTokenizer st,
379            String message) throws InvalidFormatException {
380        throw new InvalidFormatException(Messages.getString("security.8F", //$NON-NLS-1$
381                composeStatus(st), message));
382    }
383
384    /**
385     * Throws InvalidFormatException with error status: which token is
386     * unexpected on which line.
387     *
388     * @param st
389     *            a tokenizer holding the erroneous token
390     */
391    protected final void handleUnexpectedToken(StreamTokenizer st)
392            throws InvalidFormatException {
393        throw new InvalidFormatException(Messages.getString("security.90", //$NON-NLS-1$
394                composeStatus(st)));
395    }
396
397    /**
398     * Compound token representing <i>keystore </i> clause. See policy format
399     * {@link org.apache.harmony.security.fortress.DefaultPolicy description}for details.
400     *
401     * @see org.apache.harmony.security.fortress.DefaultPolicyParser
402     * @see org.apache.harmony.security.DefaultPolicyScanner
403     */
404    public static class KeystoreEntry {
405
406        /**
407         * The URL part of keystore clause.
408         */
409        public String url;
410
411        /**
412         * The typename part of keystore clause.
413         */
414        public String type;
415    }
416
417    /**
418     * Compound token representing <i>grant </i> clause. See policy format
419     * {@link org.apache.harmony.security.fortress.DefaultPolicy description}for details.
420     *
421     * @see org.apache.harmony.security.fortress.DefaultPolicyParser
422     * @see org.apache.harmony.security.DefaultPolicyScanner
423     */
424    public static class GrantEntry {
425
426        /**
427         * The signers part of grant clause. This is a comma-separated list of
428         * certificate aliases.
429         */
430        public String signers;
431
432        /**
433         * The codebase part of grant clause. This is an URL from which code
434         * originates.
435         */
436        public String codebase;
437
438        /**
439         * Collection of PrincipalEntries of grant clause.
440         */
441        public Collection<PrincipalEntry> principals;
442
443        /**
444         * Collection of PermissionEntries of grant clause.
445         */
446        public Collection<PermissionEntry> permissions;
447
448        /**
449         * Adds specified element to the <code>principals</code> collection.
450         * If collection does not exist yet, creates a new one.
451         */
452        public void addPrincipal(PrincipalEntry pe) {
453            if (principals == null) {
454                principals = new HashSet<PrincipalEntry>();
455            }
456            principals.add(pe);
457        }
458
459    }
460
461    /**
462     * Compound token representing <i>principal </i> entry of a <i>grant </i>
463     * clause. See policy format
464     * {@link org.apache.harmony.security.fortress.DefaultPolicy description}for details.
465     *
466     * @see org.apache.harmony.security.fortress.DefaultPolicyParser
467     * @see org.apache.harmony.security.DefaultPolicyScanner
468     */
469    public static class PrincipalEntry {
470
471        /**
472         * Wildcard value denotes any class and/or any name.
473         * Must be asterisk, for proper general expansion and
474         * PrivateCredentialsPermission wildcarding
475         */
476        public static final String WILDCARD = "*"; //$NON-NLS-1$
477
478        /**
479         * The classname part of principal clause.
480         */
481        public String klass;
482
483        /**
484         * The name part of principal clause.
485         */
486        public String name;
487    }
488
489    /**
490     * Compound token representing <i>permission </i> entry of a <i>grant </i>
491     * clause. See policy format
492     * {@link org.apache.harmony.security.fortress.DefaultPolicy description}for details.
493     *
494     * @see org.apache.harmony.security.fortress.DefaultPolicyParser
495     * @see org.apache.harmony.security.DefaultPolicyScanner
496     */
497    public static class PermissionEntry {
498
499        /**
500         * The classname part of permission clause.
501         */
502        public String klass;
503
504        /**
505         * The name part of permission clause.
506         */
507        public String name;
508
509        /**
510         * The actions part of permission clause.
511         */
512        public String actions;
513
514        /**
515         * The signers part of permission clause. This is a comma-separated list
516         * of certificate aliases.
517         */
518        public String signers;
519    }
520}
521