View Javadoc

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