1 //
2 // ========================================================================
3 // Copyright (c) 1995-2014 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 "some_keystore_url", "keystore_type";
46 *
47 * </pre>
48 *
49 * <pre>
50 *
51 * grant [SignedBy "signer_names"] [, CodeBase "URL"]
52 * [, Principal [principal_class_name] "principal_name"]
53 * [, Principal [principal_class_name] "principal_name"] ... {
54 * permission permission_class_name [ "target_name" ] [, "action"]
55 * [, SignedBy "signer_names"];
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 * "some_keystore_url"[, "keystore_type"];
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 "url"] | [signedby "name1,...,nameN"] |
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 ] "principal_name"
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 * [ "target_name" ] [, "action_list"]
317 * [, signedby "name1,name2,..."];
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 }