package org.eclipse.jetty.policy.loader;
//========================================================================
//Copyright (c) Webtide LLC
//------------------------------------------------------------------------
//All rights reserved. This program and the accompanying materials
//are made available under the terms of the Eclipse Public License v1.0
//and Apache License v2.0 which accompanies this distribution.
//
//The Eclipse Public License is available at
//http://www.eclipse.org/legal/epl-v10.html
//
//The Apache License v2.0 is available at
//http://www.apache.org/licenses/LICENSE-2.0.txt
//
//You may elect to redistribute this code under either of these licenses.
//========================================================================
// This file adapted for use from Apache Harmony code by written and contributed
// to that project by Alexey V. Varlamov under the ASL-2.0
// See CQ3380
//========================================================================
import java.io.IOException;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import org.eclipse.jetty.policy.entry.GrantEntry;
import org.eclipse.jetty.policy.entry.KeystoreEntry;
import org.eclipse.jetty.policy.entry.PermissionEntry;
import org.eclipse.jetty.policy.entry.PrincipalEntry;
/**
* This is a basic high-level tokenizer of policy files. It takes in a stream, analyzes data read from it and returns a
* set of structured tokens. <br>
* This implementation recognizes text files, consisting of clauses with the following syntax:
*
* <pre>
*
* keystore "some_keystore_url", "keystore_type";
*
* </pre>
*
* <pre>
*
* grant [SignedBy "signer_names"] [, CodeBase "URL"]
* [, Principal [principal_class_name] "principal_name"]
* [, Principal [principal_class_name] "principal_name"] ... {
* permission permission_class_name [ "target_name" ] [, "action"]
* [, SignedBy "signer_names"];
* permission ...
* };
*
* </pre>
*
* For semantical details of this format, see org.apache.harmony.security.DefaultPolicy javadoc. <br>
*
* Keywords are case-insensitive in contrast to quoted string literals. Comma-separation rule is quite forgiving, most
* commas may be just omitted. Whitespaces, line- and block comments are ignored. Symbol-level tokenization is delegated
* to java.io.StreamTokenizer. <br>
* <br>
* This implementation is effectively thread-safe, as it has no field references to data being processed (that is,
* passes all the data as method parameters).
*
* This implementation is a bit more strict in enforcing format then the default policy scanner as implemented in the sun jdk.
*/
public class PolicyFileScanner
{
/**
* Specific exception class to signal policy file syntax error.
*/
public static class InvalidFormatException
extends Exception
{
/**
* @serial
*/
private static final long serialVersionUID = 5789786270390222184L;
/**
* Constructor with detailed message parameter.
*/
public InvalidFormatException( String arg0 )
{
super( arg0 );
}
}
/**
* Configures passed tokenizer accordingly to supported syntax.
*/
protected StreamTokenizer configure( StreamTokenizer st )
{
st.slashSlashComments( true );
st.slashStarComments( true );
st.wordChars( '_', '_' );
st.wordChars( '$', '$' );
return st;
}
/**
* Performs the main parsing loop. Starts with creating and configuring a StreamTokenizer instance; then tries to
* recognize <i>keystore </i> or <i>grant </i> keyword. When found, invokes read method corresponding to the clause
* and collects result to the passed collection.
*
* @param r policy stream reader
* @param grantEntries a collection to accumulate parsed GrantEntries
* @param keystoreEntries a collection to accumulate parsed KeystoreEntries
* @throws IOException if stream reading failed
* @throws InvalidFormatException if unexpected or unknown token encountered
*/
public void scanStream( Reader r, Collection<GrantEntry> grantEntries, List<KeystoreEntry> keystoreEntries )
throws IOException, InvalidFormatException
{
StreamTokenizer st = configure( new StreamTokenizer( r ) );
// main parsing loop
parsing: while ( true )
{
switch ( st.nextToken() )
{
case StreamTokenizer.TT_EOF: // we've done the job
break parsing;
case StreamTokenizer.TT_WORD:
if ( Util.equalsIgnoreCase( "keystore", st.sval ) ) { //$NON-NLS-1$
keystoreEntries.add( readKeystoreNode( st ) );
}
else if ( Util.equalsIgnoreCase( "grant", st.sval ) ) { //$NON-NLS-1$
grantEntries.add( readGrantNode( st ) );
}
else
{
handleUnexpectedToken( st, "Expected entries are : \"grant\" or \"keystore\"" ); //$NON-NLS-1$
}
break;
case ';': // just delimiter of entries
break;
default:
handleUnexpectedToken( st );
break;
}
}
}
/**
* Tries to read <i>keystore </i> clause fields. The expected syntax is
*
* <pre>
*
* "some_keystore_url"[, "keystore_type"];
*
* </pre>
*
* @return successfully parsed KeystoreNode
* @throws IOException if stream reading failed
* @throws InvalidFormatException if unexpected or unknown token encountered
*/
protected KeystoreEntry readKeystoreNode( StreamTokenizer st )
throws IOException, InvalidFormatException
{
KeystoreEntry ke = new KeystoreEntry();
if ( st.nextToken() == '"' )
{
ke.setUrl( st.sval );
if ( ( st.nextToken() == '"' ) || ( ( st.ttype == ',' ) && ( st.nextToken() == '"' ) ) )
{
ke.setType( st.sval );
}
else
{ // handle token in the main loop
st.pushBack();
}
}
else
{
handleUnexpectedToken( st, "Expected syntax is : keystore \"url\"[, \"type\"]" ); //$NON-NLS-1$
}
return ke;
}
/**
* Tries to read <i>grant </i> clause. <br>
* First, it reads <i>codebase </i>, <i>signedby </i>, <i>principal </i> entries till the '{' (opening curly brace)
* symbol. Then it calls readPermissionEntries() method to read the permissions of this clause. <br>
* Principal entries (if any) are read by invoking readPrincipalNode() method, obtained PrincipalEntries are
* accumulated. <br>
* The expected syntax is
*
* <pre>
*
* [ [codebase "url"] | [signedby "name1,...,nameN"] |
* principal ...] ]* { ... }
*
* </pre>
*
* @return successfully parsed GrantNode
* @throws IOException if stream reading failed
* @throws InvalidFormatException if unexpected or unknown token encountered
*/
protected GrantEntry readGrantNode( StreamTokenizer st )
throws IOException, InvalidFormatException
{
GrantEntry ge = new GrantEntry();
parsing: while ( true )
{
switch ( st.nextToken() )
{
case StreamTokenizer.TT_WORD:
if ( Util.equalsIgnoreCase( "signedby", st.sval ) ) { //$NON-NLS-1$
if ( st.nextToken() == '"' )
{
ge.setSigners( st.sval );
}
else
{
handleUnexpectedToken( st, "Expected syntax is : signedby \"name1,...,nameN\"" ); //$NON-NLS-1$
}
}
else if ( Util.equalsIgnoreCase( "codebase", st.sval ) ) { //$NON-NLS-1$
if ( st.nextToken() == '"' )
{
ge.setCodebase( st.sval );
}
else
{
handleUnexpectedToken( st, "Expected syntax is : codebase \"url\"" ); //$NON-NLS-1$
}
}
else if ( Util.equalsIgnoreCase( "principal", st.sval ) ) { //$NON-NLS-1$
ge.addPrincipal( readPrincipalNode( st ) );
}
else
{
handleUnexpectedToken( st );
}
break;
case ',': // just delimiter of entries
break;
case '{':
ge.setPermissions( readPermissionEntries( st ) );
break parsing;
default: // handle token in the main loop
st.pushBack();
break parsing;
}
}
return ge;
}
/**
* Tries to read <i>Principal </i> Node fields. The expected syntax is
*
* <pre>
*
* [ principal_class_name ] "principal_name"
*
* </pre>
*
* Both class and name may be wildcards, wildcard names should not surrounded by quotes.
*
* @return successfully parsed PrincipalNode
* @throws IOException if stream reading failed
* @throws InvalidFormatException if unexpected or unknown token encountered
*/
protected PrincipalEntry readPrincipalNode( StreamTokenizer st )
throws IOException, InvalidFormatException
{
PrincipalEntry pe = new PrincipalEntry();
if ( st.nextToken() == StreamTokenizer.TT_WORD )
{
pe.setKlass( st.sval );
st.nextToken();
}
else if ( st.ttype == '*' )
{
pe.setKlass( PrincipalEntry.WILDCARD );
st.nextToken();
}
if ( st.ttype == '"' )
{
pe.setName( st.sval );
}
else if ( st.ttype == '*' )
{
pe.setName( PrincipalEntry.WILDCARD );
}
else
{
handleUnexpectedToken( st, "Expected syntax is : principal [class_name] \"principal_name\"" ); //$NON-NLS-1$
}
return pe;
}
/**
* Tries to read a list of <i>permission </i> entries. The expected syntax is
*
* <pre>
*
* permission permission_class_name
* [ "target_name" ] [, "action_list"]
* [, signedby "name1,name2,..."];
*
* </pre>
*
* List is terminated by '}' (closing curly brace) symbol.
*
* @return collection of successfully parsed PermissionEntries
* @throws IOException if stream reading failed
* @throws InvalidFormatException if unexpected or unknown token encountered
*/
protected Collection<PermissionEntry> readPermissionEntries( StreamTokenizer st )
throws IOException, InvalidFormatException
{
Collection<PermissionEntry> permissions = new HashSet<PermissionEntry>();
parsing: while ( true )
{
switch ( st.nextToken() )
{
case StreamTokenizer.TT_WORD:
if ( Util.equalsIgnoreCase( "permission", st.sval ) ) { //$NON-NLS-1$
PermissionEntry pe = new PermissionEntry();
if ( st.nextToken() == StreamTokenizer.TT_WORD )
{
pe.setKlass( st.sval );
if ( st.nextToken() == '"' )
{
pe.setName( st.sval );
st.nextToken();
}
if ( st.ttype == ',' )
{
st.nextToken();
}
if ( st.ttype == '"' )
{
pe.setActions( st.sval );
if ( st.nextToken() == ',' )
{
st.nextToken();
}
}
if ( st.ttype == StreamTokenizer.TT_WORD && Util.equalsIgnoreCase( "signedby", st.sval ) ) { //$NON-NLS-1$
if ( st.nextToken() == '"' )
{
pe.setSigners( st.sval );
}
else
{
handleUnexpectedToken( st );
}
}
else
{ // handle token in the next iteration
st.pushBack();
}
permissions.add( pe );
continue parsing;
}
}
handleUnexpectedToken(
st,
"Expected syntax is : permission permission_class_name [\"target_name\"] [, \"action_list\"] [, signedby \"name1,...,nameN\"]" ); //$NON-NLS-1$
break;
case ';': // just delimiter of entries
break;
case '}': // end of list
break parsing;
default: // invalid token
handleUnexpectedToken( st );
break;
}
}
return permissions;
}
/**
* Formats a detailed description of tokenizer status: current token, current line number, etc.
*/
protected String composeStatus( StreamTokenizer st )
{
return st.toString();
}
/**
* Throws InvalidFormatException with detailed diagnostics.
*
* @param st a tokenizer holding the erroneous token
* @param message a user-friendly comment, probably explaining expected syntax. Should not be <code>null</code>- use
* the overloaded single-parameter method instead.
*/
protected final void handleUnexpectedToken( StreamTokenizer st, String message )
throws InvalidFormatException
{
throw new InvalidFormatException( "Unexpected token encountered: " + composeStatus( st ) + ". " + message );
}
/**
* Throws InvalidFormatException with error status: which token is unexpected on which line.
*
* @param st a tokenizer holding the erroneous token
*/
protected final void handleUnexpectedToken( StreamTokenizer st )
throws InvalidFormatException
{
throw new InvalidFormatException( "Unexpected token encountered: " + composeStatus( st ) );
}
private static class Util
{
public static String toUpperCase( String s )
{
int len = s.length();
StringBuilder buffer = new StringBuilder( len );
for ( int i = 0; i < len; i++ )
{
char c = s.charAt( i );
if ( 'a' <= c && c <= 'z' )
{
buffer.append( (char) ( c - ( 'a' - 'A' ) ) );
}
else
{
buffer.append( c );
}
}
return buffer.toString();
}
public static boolean equalsIgnoreCase( String s1, String s2 )
{
s1 = toUpperCase( s1 );
s2 = toUpperCase( s2 );
return s1.equals( s2 );
}
}
}