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 org.apache.harmony.security.DefaultPolicy javadoc. <br>
60   * 
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   * This implementation is a bit more strict in enforcing format then the default policy scanner as implemented in the sun jdk.
69   */
70  public class PolicyFileScanner
71  {
72  
73      /**
74       * Specific exception class to signal policy file syntax error.
75       */
76      public static class InvalidFormatException
77          extends Exception
78      {
79  
80          /**
81           * @serial
82           */
83          private static final long serialVersionUID = 5789786270390222184L;
84  
85          /**
86           * Constructor with detailed message parameter.
87           */
88          public InvalidFormatException( String arg0 )
89          {
90              super( arg0 );
91          }
92      }
93  
94      /**
95       * Configures passed tokenizer accordingly to supported syntax.
96       */
97      protected StreamTokenizer configure( StreamTokenizer st )
98      {
99          st.slashSlashComments( true );
100         st.slashStarComments( true );
101         st.wordChars( '_', '_' );
102         st.wordChars( '$', '$' );
103         return st;
104     }
105 
106     /**
107      * Performs the main parsing loop. Starts with creating and configuring a StreamTokenizer instance; then tries to
108      * recognize <i>keystore </i> or <i>grant </i> keyword. When found, invokes read method corresponding to the clause
109      * and collects result to the passed collection.
110      * 
111      * @param r policy stream reader
112      * @param grantEntries a collection to accumulate parsed GrantEntries
113      * @param keystoreEntries a collection to accumulate parsed KeystoreEntries
114      * @throws IOException if stream reading failed
115      * @throws InvalidFormatException if unexpected or unknown token encountered
116      */
117     public void scanStream( Reader r, Collection<GrantEntry> grantEntries, List<KeystoreEntry> keystoreEntries )
118         throws IOException, InvalidFormatException
119     {
120         StreamTokenizer st = configure( new StreamTokenizer( r ) );
121         // main parsing loop
122         parsing: while ( true )
123         {
124             switch ( st.nextToken() )
125             {
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( readKeystoreNode( st ) );
132                     }
133                     else if ( Util.equalsIgnoreCase( "grant", st.sval ) ) { //$NON-NLS-1$
134                         grantEntries.add( readGrantNode( st ) );
135                     }
136                     else
137                     {
138                         handleUnexpectedToken( st, "Expected entries are : \"grant\" or \"keystore\"" ); //$NON-NLS-1$
139 
140                     }
141                     break;
142 
143                 case ';': // just delimiter of entries
144                     break;
145 
146                 default:
147                     handleUnexpectedToken( st );
148                     break;
149             }
150         }
151     }
152 
153     /**
154      * Tries to read <i>keystore </i> clause fields. The expected syntax is
155      * 
156      * <pre>
157      * 
158      *     &quot;some_keystore_url&quot;[, &quot;keystore_type&quot;];
159      * 
160      * </pre>
161      * 
162      * @return successfully parsed KeystoreNode
163      * @throws IOException if stream reading failed
164      * @throws InvalidFormatException if unexpected or unknown token encountered
165      */
166     protected KeystoreEntry readKeystoreNode( StreamTokenizer st )
167         throws IOException, InvalidFormatException
168     {
169         KeystoreEntry ke = new KeystoreEntry();
170         if ( st.nextToken() == '"' )
171         {
172             ke.setUrl( st.sval );
173             if ( ( st.nextToken() == '"' ) || ( ( st.ttype == ',' ) && ( st.nextToken() == '"' ) ) )
174             {
175                 ke.setType( st.sval );
176             }
177             else
178             { // handle token in the main loop
179                 st.pushBack();
180             }
181         }
182         else
183         {
184             handleUnexpectedToken( st, "Expected syntax is : keystore \"url\"[, \"type\"]" ); //$NON-NLS-1$
185 
186         }
187         return ke;
188     }
189 
190     /**
191      * Tries to read <i>grant </i> clause. <br>
192      * First, it reads <i>codebase </i>, <i>signedby </i>, <i>principal </i> entries till the '{' (opening curly brace)
193      * symbol. Then it calls readPermissionEntries() method to read the permissions of this clause. <br>
194      * Principal entries (if any) are read by invoking readPrincipalNode() method, obtained PrincipalEntries are
195      * accumulated. <br>
196      * The expected syntax is
197      * 
198      * <pre>
199      * 
200      *     [ [codebase &quot;url&quot;] | [signedby &quot;name1,...,nameN&quot;] | 
201      *          principal ...] ]* { ... }
202      * 
203      * </pre>
204      * 
205      * @return successfully parsed GrantNode
206      * @throws IOException if stream reading failed
207      * @throws InvalidFormatException if unexpected or unknown token encountered
208      */
209     protected GrantEntry readGrantNode( StreamTokenizer st )
210         throws IOException, InvalidFormatException
211     {
212         GrantEntry ge = new GrantEntry();
213         parsing: while ( true )
214         {
215             switch ( st.nextToken() )
216             {
217 
218                 case StreamTokenizer.TT_WORD:
219                     if ( Util.equalsIgnoreCase( "signedby", st.sval ) ) { //$NON-NLS-1$
220                         if ( st.nextToken() == '"' )
221                         {
222                             ge.setSigners( st.sval );
223                         }
224                         else
225                         {
226                             handleUnexpectedToken( st, "Expected syntax is : signedby \"name1,...,nameN\"" ); //$NON-NLS-1$
227                         }
228                     }
229                     else if ( Util.equalsIgnoreCase( "codebase", st.sval ) ) { //$NON-NLS-1$
230                         if ( st.nextToken() == '"' )
231                         {
232                             ge.setCodebase( st.sval );
233                         }
234                         else
235                         {
236                             handleUnexpectedToken( st, "Expected syntax is : codebase \"url\"" ); //$NON-NLS-1$
237                         }
238                     }
239                     else if ( Util.equalsIgnoreCase( "principal", st.sval ) ) { //$NON-NLS-1$
240                         ge.addPrincipal( readPrincipalNode( st ) );
241                     }
242                     else
243                     {
244                         handleUnexpectedToken( st );
245                     }
246                     break;
247 
248                 case ',': // just delimiter of entries
249                     break;
250 
251                 case '{':
252                     ge.setPermissions( readPermissionEntries( st ) );
253                     break parsing;
254 
255                 default: // handle token in the main loop
256                     st.pushBack();
257                     break parsing;
258             }
259         }
260 
261         return ge;
262     }
263 
264     /**
265      * Tries to read <i>Principal </i> Node fields. The expected syntax is
266      * 
267      * <pre>
268      * 
269      *     [ principal_class_name ] &quot;principal_name&quot;
270      * 
271      * </pre>
272      * 
273      * Both class and name may be wildcards, wildcard names should not surrounded by quotes.
274      * 
275      * @return successfully parsed PrincipalNode
276      * @throws IOException if stream reading failed
277      * @throws InvalidFormatException if unexpected or unknown token encountered
278      */
279     protected PrincipalEntry readPrincipalNode( StreamTokenizer st )
280         throws IOException, InvalidFormatException
281     {
282         PrincipalEntry pe = new PrincipalEntry();
283         if ( st.nextToken() == StreamTokenizer.TT_WORD )
284         {
285             pe.setKlass( st.sval );
286             st.nextToken();
287         }
288         else if ( st.ttype == '*' )
289         {
290             pe.setKlass( PrincipalEntry.WILDCARD );
291             st.nextToken();
292         }
293         if ( st.ttype == '"' )
294         {
295             pe.setName( st.sval );
296         }
297         else if ( st.ttype == '*' )
298         {
299             pe.setName( PrincipalEntry.WILDCARD );
300         }
301         else
302         {
303             handleUnexpectedToken( st, "Expected syntax is : principal [class_name] \"principal_name\"" ); //$NON-NLS-1$
304         }
305         return pe;
306     }
307 
308     /**
309      * Tries to read a list of <i>permission </i> entries. The expected syntax is
310      * 
311      * <pre>
312      * 
313      *     permission permission_class_name
314      *          [ &quot;target_name&quot; ] [, &quot;action_list&quot;]
315      *          [, signedby &quot;name1,name2,...&quot;];
316      * 
317      * </pre>
318      * 
319      * List is terminated by '}' (closing curly brace) symbol.
320      * 
321      * @return collection of successfully parsed PermissionEntries
322      * @throws IOException if stream reading failed
323      * @throws InvalidFormatException if unexpected or unknown token encountered
324      */
325     protected Collection<PermissionEntry> readPermissionEntries( StreamTokenizer st )
326         throws IOException, InvalidFormatException
327     {
328         Collection<PermissionEntry> permissions = new HashSet<PermissionEntry>();
329         parsing: while ( true )
330         {
331             switch ( st.nextToken() )
332             {
333 
334                 case StreamTokenizer.TT_WORD:
335                     if ( Util.equalsIgnoreCase( "permission", st.sval ) ) { //$NON-NLS-1$
336                         PermissionEntry pe = new PermissionEntry();
337                         if ( st.nextToken() == StreamTokenizer.TT_WORD )
338                         {
339                             pe.setKlass( st.sval );
340                             if ( st.nextToken() == '"' )
341                             {
342                                 pe.setName( st.sval );
343                                 st.nextToken();
344                             }
345                             if ( st.ttype == ',' )
346                             {
347                                 st.nextToken();
348                             }
349                             if ( st.ttype == '"' )
350                             {
351                                 pe.setActions( st.sval );
352                                 if ( st.nextToken() == ',' )
353                                 {
354                                     st.nextToken();
355                                 }
356                             }
357                             if ( st.ttype == StreamTokenizer.TT_WORD && Util.equalsIgnoreCase( "signedby", st.sval ) ) { //$NON-NLS-1$
358                                 if ( st.nextToken() == '"' )
359                                 {
360                                     pe.setSigners( st.sval );
361                                 }
362                                 else
363                                 {
364                                     handleUnexpectedToken( st );
365                                 }
366                             }
367                             else
368                             { // handle token in the next iteration
369                                 st.pushBack();
370                             }
371                             permissions.add( pe );
372                             continue parsing;
373                         }
374                     }
375                     handleUnexpectedToken(
376                                            st,
377                                            "Expected syntax is : permission permission_class_name [\"target_name\"] [, \"action_list\"] [, signedby \"name1,...,nameN\"]" ); //$NON-NLS-1$
378                     break;
379 
380                 case ';': // just delimiter of entries
381                     break;
382 
383                 case '}': // end of list
384                     break parsing;
385 
386                 default: // invalid token
387                     handleUnexpectedToken( st );
388                     break;
389             }
390         }
391 
392         return permissions;
393     }
394 
395     /**
396      * Formats a detailed description of tokenizer status: current token, current line number, etc.
397      */
398     protected String composeStatus( StreamTokenizer st )
399     {
400         return st.toString();
401     }
402 
403     /**
404      * Throws InvalidFormatException with detailed diagnostics.
405      * 
406      * @param st a tokenizer holding the erroneous token
407      * @param message a user-friendly comment, probably explaining expected syntax. Should not be <code>null</code>- use
408      *            the overloaded single-parameter method instead.
409      */
410     protected final void handleUnexpectedToken( StreamTokenizer st, String message )
411         throws InvalidFormatException
412     {
413         throw new InvalidFormatException( "Unexpected token encountered: " + composeStatus( st ) + ". " + message );
414     }
415 
416     /**
417      * Throws InvalidFormatException with error status: which token is unexpected on which line.
418      * 
419      * @param st a tokenizer holding the erroneous token
420      */
421     protected final void handleUnexpectedToken( StreamTokenizer st )
422         throws InvalidFormatException
423     {
424         throw new InvalidFormatException( "Unexpected token encountered: " + composeStatus( st ) );
425     }
426 
427 
428     private static class Util
429     {
430         public static String toUpperCase( String s )
431         {
432             int len = s.length();
433             StringBuilder buffer = new StringBuilder( len );
434             for ( int i = 0; i < len; i++ )
435             {
436                 char c = s.charAt( i );
437                 if ( 'a' <= c && c <= 'z' )
438                 {
439                     buffer.append( (char) ( c - ( 'a' - 'A' ) ) );
440                 }
441                 else
442                 {
443                     buffer.append( c );
444                 }
445             }
446             return buffer.toString();
447         }
448 
449         public static boolean equalsIgnoreCase( String s1, String s2 )
450         {
451             s1 = toUpperCase( s1 );
452             s2 = toUpperCase( s2 );
453             return s1.equals( s2 );
454         }
455     }
456 
457 }