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 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 * "some_keystore_url"[, "keystore_type"];
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 "url"] | [signedby "name1,...,nameN"] |
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 ] "principal_name"
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 * [ "target_name" ] [, "action_list"]
315 * [, signedby "name1,name2,..."];
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 }