View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //  This file adapted for use from Apache Harmony code by written and contributed 
18  //  to that project by Alexey V. Varlamov under the ASL-2.0
19  //  See CQ3380
20  //  ========================================================================
21  
22  package org.eclipse.jetty.policy.loader;
23  
24  
25  import java.io.IOException;
26  import java.io.Reader;
27  import java.io.StreamTokenizer;
28  import java.util.Collection;
29  import java.util.HashSet;
30  import java.util.List;
31  
32  import org.eclipse.jetty.policy.entry.GrantEntry;
33  import org.eclipse.jetty.policy.entry.KeystoreEntry;
34  import org.eclipse.jetty.policy.entry.PermissionEntry;
35  import org.eclipse.jetty.policy.entry.PrincipalEntry;
36  
37  
38  /**
39   * This is a basic high-level tokenizer of policy files. It takes in a stream, analyzes data read from it and returns a
40   * set of structured tokens. <br>
41   * This implementation recognizes text files, consisting of clauses with the following syntax:
42   * 
43   * <pre>
44   * 
45   *     keystore &quot;some_keystore_url&quot;, &quot;keystore_type&quot;;
46   * 
47   * </pre>
48   * 
49   * <pre>
50   * 
51   *     grant [SignedBy &quot;signer_names&quot;] [, CodeBase &quot;URL&quot;]
52   *      [, Principal [principal_class_name] &quot;principal_name&quot;]
53   *      [, Principal [principal_class_name] &quot;principal_name&quot;] ... {
54   *      permission permission_class_name [ &quot;target_name&quot; ] [, &quot;action&quot;] 
55   *      [, SignedBy &quot;signer_names&quot;];
56   *      permission ...
57   *      };
58   * 
59   * </pre>
60   * 
61   * For semantical details of this format, see org.apache.harmony.security.DefaultPolicy javadoc. <br>
62   * 
63   * Keywords are case-insensitive in contrast to quoted string literals. Comma-separation rule is quite forgiving, most
64   * commas may be just omitted. Whitespaces, line- and block comments are ignored. Symbol-level tokenization is delegated
65   * to java.io.StreamTokenizer. <br>
66   * <br>
67   * This implementation is effectively thread-safe, as it has no field references to data being processed (that is,
68   * passes all the data as method parameters).
69   * 
70   * This implementation is a bit more strict in enforcing format then the default policy scanner as implemented in the sun jdk.
71   */
72  public class PolicyFileScanner
73  {
74  
75      /**
76       * Specific exception class to signal policy file syntax error.
77       */
78      public static class InvalidFormatException
79          extends Exception
80      {
81  
82          /**
83           * @serial
84           */
85          private static final long serialVersionUID = 5789786270390222184L;
86  
87          /**
88           * Constructor with detailed message parameter.
89           */
90          public InvalidFormatException( String arg0 )
91          {
92              super( arg0 );
93          }
94      }
95  
96      /**
97       * Configures passed tokenizer accordingly to supported syntax.
98       */
99      protected StreamTokenizer configure( StreamTokenizer st )
100     {
101         st.slashSlashComments( true );
102         st.slashStarComments( true );
103         st.wordChars( '_', '_' );
104         st.wordChars( '$', '$' );
105         return st;
106     }
107 
108     /**
109      * Performs the main parsing loop. Starts with creating and configuring a StreamTokenizer instance; then tries to
110      * recognize <i>keystore </i> or <i>grant </i> keyword. When found, invokes read method corresponding to the clause
111      * and collects result to the passed collection.
112      * 
113      * @param r policy stream reader
114      * @param grantEntries a collection to accumulate parsed GrantEntries
115      * @param keystoreEntries a collection to accumulate parsed KeystoreEntries
116      * @throws IOException if stream reading failed
117      * @throws InvalidFormatException if unexpected or unknown token encountered
118      */
119     public void scanStream( Reader r, Collection<GrantEntry> grantEntries, List<KeystoreEntry> keystoreEntries )
120         throws IOException, InvalidFormatException
121     {
122         StreamTokenizer st = configure( new StreamTokenizer( r ) );
123         // main parsing loop
124         parsing: while ( true )
125         {
126             switch ( st.nextToken() )
127             {
128                 case StreamTokenizer.TT_EOF: // we've done the job
129                     break parsing;
130 
131                 case StreamTokenizer.TT_WORD:
132                     if ( Util.equalsIgnoreCase( "keystore", st.sval ) ) { //$NON-NLS-1$
133                         keystoreEntries.add( readKeystoreNode( st ) );
134                     }
135                     else if ( Util.equalsIgnoreCase( "grant", st.sval ) ) { //$NON-NLS-1$
136                         grantEntries.add( readGrantNode( st ) );
137                     }
138                     else
139                     {
140                         handleUnexpectedToken( st, "Expected entries are : \"grant\" or \"keystore\"" ); //$NON-NLS-1$
141 
142                     }
143                     break;
144 
145                 case ';': // just delimiter of entries
146                     break;
147 
148                 default:
149                     handleUnexpectedToken( st );
150                     break;
151             }
152         }
153     }
154 
155     /**
156      * Tries to read <i>keystore </i> clause fields. The expected syntax is
157      * 
158      * <pre>
159      * 
160      *     &quot;some_keystore_url&quot;[, &quot;keystore_type&quot;];
161      * 
162      * </pre>
163      * 
164      * @return successfully parsed KeystoreNode
165      * @throws IOException if stream reading failed
166      * @throws InvalidFormatException if unexpected or unknown token encountered
167      */
168     protected KeystoreEntry readKeystoreNode( StreamTokenizer st )
169         throws IOException, InvalidFormatException
170     {
171         KeystoreEntry ke = new KeystoreEntry();
172         if ( st.nextToken() == '"' )
173         {
174             ke.setUrl( st.sval );
175             if ( ( st.nextToken() == '"' ) || ( ( st.ttype == ',' ) && ( st.nextToken() == '"' ) ) )
176             {
177                 ke.setType( st.sval );
178             }
179             else
180             { // handle token in the main loop
181                 st.pushBack();
182             }
183         }
184         else
185         {
186             handleUnexpectedToken( st, "Expected syntax is : keystore \"url\"[, \"type\"]" ); //$NON-NLS-1$
187 
188         }
189         return ke;
190     }
191 
192     /**
193      * Tries to read <i>grant </i> clause. <br>
194      * First, it reads <i>codebase </i>, <i>signedby </i>, <i>principal </i> entries till the '{' (opening curly brace)
195      * symbol. Then it calls readPermissionEntries() method to read the permissions of this clause. <br>
196      * Principal entries (if any) are read by invoking readPrincipalNode() method, obtained PrincipalEntries are
197      * accumulated. <br>
198      * The expected syntax is
199      * 
200      * <pre>
201      * 
202      *     [ [codebase &quot;url&quot;] | [signedby &quot;name1,...,nameN&quot;] | 
203      *          principal ...] ]* { ... }
204      * 
205      * </pre>
206      * 
207      * @return successfully parsed GrantNode
208      * @throws IOException if stream reading failed
209      * @throws InvalidFormatException if unexpected or unknown token encountered
210      */
211     protected GrantEntry readGrantNode( StreamTokenizer st )
212         throws IOException, InvalidFormatException
213     {
214         GrantEntry ge = new GrantEntry();
215         parsing: while ( true )
216         {
217             switch ( st.nextToken() )
218             {
219 
220                 case StreamTokenizer.TT_WORD:
221                     if ( Util.equalsIgnoreCase( "signedby", st.sval ) ) { //$NON-NLS-1$
222                         if ( st.nextToken() == '"' )
223                         {
224                             ge.setSigners( st.sval );
225                         }
226                         else
227                         {
228                             handleUnexpectedToken( st, "Expected syntax is : signedby \"name1,...,nameN\"" ); //$NON-NLS-1$
229                         }
230                     }
231                     else if ( Util.equalsIgnoreCase( "codebase", st.sval ) ) { //$NON-NLS-1$
232                         if ( st.nextToken() == '"' )
233                         {
234                             ge.setCodebase( st.sval );
235                         }
236                         else
237                         {
238                             handleUnexpectedToken( st, "Expected syntax is : codebase \"url\"" ); //$NON-NLS-1$
239                         }
240                     }
241                     else if ( Util.equalsIgnoreCase( "principal", st.sval ) ) { //$NON-NLS-1$
242                         ge.addPrincipal( readPrincipalNode( st ) );
243                     }
244                     else
245                     {
246                         handleUnexpectedToken( st );
247                     }
248                     break;
249 
250                 case ',': // just delimiter of entries
251                     break;
252 
253                 case '{':
254                     ge.setPermissions( readPermissionEntries( st ) );
255                     break parsing;
256 
257                 default: // handle token in the main loop
258                     st.pushBack();
259                     break parsing;
260             }
261         }
262 
263         return ge;
264     }
265 
266     /**
267      * Tries to read <i>Principal </i> Node fields. The expected syntax is
268      * 
269      * <pre>
270      * 
271      *     [ principal_class_name ] &quot;principal_name&quot;
272      * 
273      * </pre>
274      * 
275      * Both class and name may be wildcards, wildcard names should not surrounded by quotes.
276      * 
277      * @return successfully parsed PrincipalNode
278      * @throws IOException if stream reading failed
279      * @throws InvalidFormatException if unexpected or unknown token encountered
280      */
281     protected PrincipalEntry readPrincipalNode( StreamTokenizer st )
282         throws IOException, InvalidFormatException
283     {
284         PrincipalEntry pe = new PrincipalEntry();
285         if ( st.nextToken() == StreamTokenizer.TT_WORD )
286         {
287             pe.setKlass( st.sval );
288             st.nextToken();
289         }
290         else if ( st.ttype == '*' )
291         {
292             pe.setKlass( PrincipalEntry.WILDCARD );
293             st.nextToken();
294         }
295         if ( st.ttype == '"' )
296         {
297             pe.setName( st.sval );
298         }
299         else if ( st.ttype == '*' )
300         {
301             pe.setName( PrincipalEntry.WILDCARD );
302         }
303         else
304         {
305             handleUnexpectedToken( st, "Expected syntax is : principal [class_name] \"principal_name\"" ); //$NON-NLS-1$
306         }
307         return pe;
308     }
309 
310     /**
311      * Tries to read a list of <i>permission </i> entries. The expected syntax is
312      * 
313      * <pre>
314      * 
315      *     permission permission_class_name
316      *          [ &quot;target_name&quot; ] [, &quot;action_list&quot;]
317      *          [, signedby &quot;name1,name2,...&quot;];
318      * 
319      * </pre>
320      * 
321      * List is terminated by '}' (closing curly brace) symbol.
322      * 
323      * @return collection of successfully parsed PermissionEntries
324      * @throws IOException if stream reading failed
325      * @throws InvalidFormatException if unexpected or unknown token encountered
326      */
327     protected Collection<PermissionEntry> readPermissionEntries( StreamTokenizer st )
328         throws IOException, InvalidFormatException
329     {
330         Collection<PermissionEntry> permissions = new HashSet<PermissionEntry>();
331         parsing: while ( true )
332         {
333             switch ( st.nextToken() )
334             {
335 
336                 case StreamTokenizer.TT_WORD:
337                     if ( Util.equalsIgnoreCase( "permission", st.sval ) ) { //$NON-NLS-1$
338                         PermissionEntry pe = new PermissionEntry();
339                         if ( st.nextToken() == StreamTokenizer.TT_WORD )
340                         {
341                             pe.setKlass( st.sval );
342                             if ( st.nextToken() == '"' )
343                             {
344                                 pe.setName( st.sval );
345                                 st.nextToken();
346                             }
347                             if ( st.ttype == ',' )
348                             {
349                                 st.nextToken();
350                             }
351                             if ( st.ttype == '"' )
352                             {
353                                 pe.setActions( st.sval );
354                                 if ( st.nextToken() == ',' )
355                                 {
356                                     st.nextToken();
357                                 }
358                             }
359                             if ( st.ttype == StreamTokenizer.TT_WORD && Util.equalsIgnoreCase( "signedby", st.sval ) ) { //$NON-NLS-1$
360                                 if ( st.nextToken() == '"' )
361                                 {
362                                     pe.setSigners( st.sval );
363                                 }
364                                 else
365                                 {
366                                     handleUnexpectedToken( st );
367                                 }
368                             }
369                             else
370                             { // handle token in the next iteration
371                                 st.pushBack();
372                             }
373                             permissions.add( pe );
374                             continue parsing;
375                         }
376                     }
377                     handleUnexpectedToken(
378                                            st,
379                                            "Expected syntax is : permission permission_class_name [\"target_name\"] [, \"action_list\"] [, signedby \"name1,...,nameN\"]" ); //$NON-NLS-1$
380                     break;
381 
382                 case ';': // just delimiter of entries
383                     break;
384 
385                 case '}': // end of list
386                     break parsing;
387 
388                 default: // invalid token
389                     handleUnexpectedToken( st );
390                     break;
391             }
392         }
393 
394         return permissions;
395     }
396 
397     /**
398      * Formats a detailed description of tokenizer status: current token, current line number, etc.
399      */
400     protected String composeStatus( StreamTokenizer st )
401     {
402         return st.toString();
403     }
404 
405     /**
406      * Throws InvalidFormatException with detailed diagnostics.
407      * 
408      * @param st a tokenizer holding the erroneous token
409      * @param message a user-friendly comment, probably explaining expected syntax. Should not be <code>null</code>- use
410      *            the overloaded single-parameter method instead.
411      */
412     protected final void handleUnexpectedToken( StreamTokenizer st, String message )
413         throws InvalidFormatException
414     {
415         throw new InvalidFormatException( "Unexpected token encountered: " + composeStatus( st ) + ". " + message );
416     }
417 
418     /**
419      * Throws InvalidFormatException with error status: which token is unexpected on which line.
420      * 
421      * @param st a tokenizer holding the erroneous token
422      */
423     protected final void handleUnexpectedToken( StreamTokenizer st )
424         throws InvalidFormatException
425     {
426         throw new InvalidFormatException( "Unexpected token encountered: " + composeStatus( st ) );
427     }
428 
429 
430     private static class Util
431     {
432         public static String toUpperCase( String s )
433         {
434             int len = s.length();
435             StringBuilder buffer = new StringBuilder( len );
436             for ( int i = 0; i < len; i++ )
437             {
438                 char c = s.charAt( i );
439                 if ( 'a' <= c && c <= 'z' )
440                 {
441                     buffer.append( (char) ( c - ( 'a' - 'A' ) ) );
442                 }
443                 else
444                 {
445                     buffer.append( c );
446                 }
447             }
448             return buffer.toString();
449         }
450 
451         public static boolean equalsIgnoreCase( String s1, String s2 )
452         {
453             s1 = toUpperCase( s1 );
454             s2 = toUpperCase( s2 );
455             return s1.equals( s2 );
456         }
457     }
458 
459 }