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 "some_keystore_url", "keystore_type";
44 *
45 * </pre>
46 *
47 * <pre>
48 *
49 * grant [SignedBy "signer_names"] [, CodeBase "URL"]
50 * [, Principal [principal_class_name] "principal_name"]
51 * [, Principal [principal_class_name] "principal_name"] ... {
52 * permission permission_class_name [ "target_name" ] [, "action"]
53 * [, SignedBy "signer_names"];
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 * "some_keystore_url"[, "keystore_type"];
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 "url"] | [signedby "name1,...,nameN"] |
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 ] "principal_name"
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 * [ "target_name" ] [, "action_list"]
314 * [, signedby "name1,name2,..."];
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 }