// DigestAuthFilter.java
// $Id: DigestAuthFilter.java,v 1.1 1996/10/09 15:58:44 frystyk Exp $
// $Id: DigestAuthFilter.java,v 1.1 1996/10/09 15:58:44 frystyk Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.jigsaw.auth;

import java.io.*;
import java.util.*;
import java.net.*;


import w3c.jigsaw.http.*;
import w3c.jigsaw.html.*;
import w3c.jigsaw.resources.*;
import w3c.tools.codec.* ;
import w3c.tools.crypt.Md5;

/**
 * A digest authentication filter.
 * This filter will use both IP and basic authentication to try to authenticate
 * incomming request. It should not be use for big user's database (typically
 * the ones that have more than 1000 entries).
 */

class DigestAuthContextException extends Exception {
  DigestAuthContextException (String msg) {
    super (msg) ;
  }
}

class DigestAuthContext {
  public String user = null;
  public String response = null;
  public String uri = null;
  public String realm = null;
  public String nonce = null;
  
  private String user_ = null;
  private String response_ = null;
  private String uri_ = null;
  private String realm_ = null;
  private String nonce_ = null;
  
  
  
  DigestAuthContext (Request request) 
       throws DigestAuthContextException, HTTPException
  {
    String info = request.getAuthorization() ; 
    int    i = 7;
    
    if ( ! info.startsWith ("Digest") ) {
      String msg = "Invalid authentication scheme (should be Digest)." ;
      throw new DigestAuthContextException (msg) ;
    }
    // Decode the credentials:
    info = info.substring(i);
    StringTokenizer Tobe_Tokenized = new StringTokenizer(info, ",");
    while (Tobe_Tokenized.hasMoreTokens())
      {
	String token = Tobe_Tokenized.nextToken();
	if (token.startsWith(" "))
	  {token = token.substring(1);}

	if (token.startsWith("username"))
	  {
	    user_ = token.substring(token.indexOf("\"") + 1);
	    user_ = user_.substring(0, user_.indexOf("\""));
	  }
	else if (token.startsWith("realm"))
	  {
	    realm_ = token.substring(token.indexOf("\"") + 1);
	    realm_ = realm_.substring(0, realm_.indexOf("\""));
	  }
	else if (token.startsWith("nonce"))
	  {
	    nonce_ = token.substring(token.indexOf("\"") + 1);
	    nonce_ = nonce_.substring(0, nonce_.indexOf("\""));
	  }
	else if (token.startsWith("response"))
	  {
	    response_ = token.substring(token.indexOf("\"") + 1);
	    response_ = response_.substring(0, response_.indexOf("\""));
	  }
	else if (token.startsWith("uri"))
	  {
	    uri_ = token.substring(token.indexOf("\"") + 1);
	    uri_ = uri_.substring(0, uri_.indexOf("\""));
	  }
      }
    
    if ((realm_ != null) && (nonce_ != null) && (response_ != null) && (user_ != null) && (uri_ != null) )
      {
	this.realm = realm_;
	this.uri = uri_;
	this.nonce = nonce_;
	this.response = response_;
	this.user = user_;
      }
    else      {
      String msg = "Invalid credentials syntax in Authorization field";
      throw new DigestAuthContextException (msg);
    }
  }
}






/**
 * DigestAuthFilter provides for both IP and basic authentication.
 * This is really a first implementation. It looses on several points:
 * <ul>
 * <li>AuthUser instances, being a subclass of resource dump their classes
 * along with their attributes, although here we know that they will all
 * be instances of AuthUser.
 * <li>The way that the nonces are stored are inefficient, needs better sorting mechanism.
 * <li>Nonce are stored in vectors, will move to classes.
 * <li>The way the ipmatcher is maintained doesn't make much sense.
 * <li>The way groups are handled is no good.
 * <li>The SimpleResourceStore is not an adequat store for the user database,
 * it should rather use the jdbmResourceStore (not written yet).
 * </ul>
 * However, this provides for the basic functionnalities.
 */

public class DigestAuthFilter extends AuthFilter {
  /**
   * Attribute index - The list of allowed users.
   */
  protected static int ATTR_ALLOWED_USERS = -1 ;
  /**
   * Attribute index - The list of allowed groups.
   */
  protected static int ATTR_ALLOWED_GROUPS = -1 ;
 
  /**
   *Attribute index - The list of allowed nonces.
   */
 protected static int ATTR_ALLOWED_NONCES = -1;
  
  static {
      Attribute   a = null ;
      Class     cls = null ;
      try {
	cls = Class.forName("w3c.jigsaw.auth.DigestAuthFilter");
      } catch (Exception ex) {
	ex.printStackTrace() ;
	System.exit(1) ;
      }
      // The list of allowed users
	a = new StringArrayAttribute("users"
				     , null
				     , Attribute.EDITABLE) ;
	ATTR_ALLOWED_USERS = AttributeRegistery.registerAttribute(cls, a) ;
	// The list of allowed groups:
	a = new StringArrayAttribute("groups"
				     , null
				     , Attribute.EDITABLE);
	ATTR_ALLOWED_GROUPS = AttributeRegistery.registerAttribute(cls, a) ;

	// The list of allowed nonces:
	a = new NonceAttribute("nonces"
			       ,"java.util.Vector"
			       , null
			       , Attribute.COMPUTED|Attribute.DONTSAVE);
	ATTR_ALLOWED_NONCES = AttributeRegistery.registerAttribute(cls, a);
  }
  
  /**
   * The IPMatcher to match IP templates to user records.
   */
  protected IPMatcher ipmatcher = null ;
  /**
   * The catalog of realms that make our scope.
   */ 
  protected RealmsCatalog catalog = null ;
  /**
   * Our associated realm.
   */
  protected AuthRealm realm = null ;
  /**
   * The nam of the realm we cache in <code>realm</code>.
   */
  protected String loaded_realm = null ;
  
  /**
   * Get a pointer to our realm, and initialize our ipmatcher.
   */
  
  protected synchronized void acquireRealm() {
    // Get our catalog:
    if ( catalog == null ) {
      httpd server = ((HTTPResource) getTargetResource()).getServer() ;
      catalog = server.getRealmsCatalog() ;
    }
    // Check that our realm name is valid:
    String name = getRealm() ;
    if ( name == null )
      return ;
    if ((realm != null) && name.equals(loaded_realm)) 
      return ;
    // Load the realm and create the ipmtacher object
    realm     = catalog.loadRealm(name) ;
    ipmatcher = new IPMatcher() ;
    Enumeration enum = realm.enumerateUserNames() ;
    while (enum.hasMoreElements()) {
      String   uname = (String) enum.nextElement() ;
      AuthUser user  = (AuthUser) realm.loadUser(uname) ;
      short    ips[][] = user.getIPTemplates() ;
      if ( ips != null ) {
	for (int i = 0 ; i < ips.length ; i++) 
	  ipmatcher.add(ips[i], user) ;
      }
    }
  }

  /**
   *Checks to see whether or not the a valid preexisting nonce is coming in from a specific IP.
   *@param ip The clients ip address.
   *@returns A Vector containing two objects, a boolean specifying whether or not it succeeded, and a nonce if it did succeed and is null if it didn't.
   */
  
  public Vector checkIP_Nonce(InetAddress ip, Vector allowed_nonces)
  {
    Boolean flag = new Boolean(false);
    String nonce = null;
    Vector return_vector = new Vector(2);
    
    Date current_time = new Date();
    
    int i = 0;
    
    while (i < allowed_nonces.size())
      {
  	Vector current_nonce = (Vector) allowed_nonces.elementAt(i);
  	String Vector_nonce = (String) current_nonce.elementAt(0);
  	Date Vector_date = (Date) current_nonce.elementAt(1);
  	InetAddress Vector_ip = (InetAddress) current_nonce.elementAt(2);
  	if (ip.equals(Vector_ip))
	  if (checkTime(current_time, Vector_date))
	    {
	      flag = new Boolean(true);
	      nonce = Vector_nonce;
	    }
	  else 
	    {
	      allowed_nonces.removeElementAt(i);
	    }
  	i++;
      }
    return_vector.insertElementAt(flag, 0);
    return_vector.insertElementAt(nonce, 1);
    return return_vector;
    
  }
  
  
  /**
   *Creates a new nonce or issues the same one depending on whether or not an existing nonce is still valid for that IP.
   *@param request The client's request.
   *@return String containing the proper nonce.
   */  
  
  private synchronized String createNonce(Request request) {
    InetAddress IP  = request.getClient().getInetAddress();
    Date date = new Date();
    Md5 creator = new Md5(IP.toString()+date.toString()+"myprivatekey");
    creator.processString();
    String  nonce = creator.getStringDigest();

    Vector element = new Vector(3);
    element.insertElementAt(nonce, 0);     
    element.insertElementAt(date, 1);    
    element.insertElementAt(IP, 2);
    
    if (getNonces() == null)
      {
    	Vector allowed_nonces = new Vector(1, 1);
    	allowed_nonces.addElement(element);
    	setValue(ATTR_ALLOWED_NONCES, allowed_nonces);
	return nonce;
      }
    else
      {
	Vector checkNonceVector = checkIP_Nonce(IP, getNonces());
      	Boolean IPcheck = (Boolean) checkNonceVector.elementAt(0);
      	if (IPcheck.booleanValue())
      	  {	    
	    return (String) checkNonceVector.elementAt(1);
      	  }
      	else
	  {
	    Vector allowed_nonces = getNonces();
	    allowed_nonces.addElement(element);
	    setValue(ATTR_ALLOWED_NONCES, allowed_nonces);
	    return nonce;
	  }
      }
  }    
    
    
    
       
    

  
  /**
   * Check that our realm does exist.
   * Otherwise we are probably being initialized, and we don't authenticate
   * yet.
   * @return A boolean <strong>true</strong> if realm can be initialized.
   */
  
  protected synchronized boolean checkRealm() {
    acquireRealm() ;
    return (ipmatcher != null) ;
  }
  
  /**
   * Get the list of allowed users.
   */
  
  public String[] getAllowedUsers() {
    return (String[]) getValue(ATTR_ALLOWED_USERS, null) ;
  }
  
  /**
   * Get the list of allowed groups.
   */
  
  public String[] getAllowedGroups() {
    return (String[]) getValue(ATTR_ALLOWED_GROUPS, null) ;
  }
  
  /**
   *Gets the list of allowed nonces.
   */
  
  public Vector getNonces() {
    return (Vector) getValue(ATTR_ALLOWED_NONCES, null);
  }
  
  /**
   * Lookup a user by its IP address.
   * @param ipaddr The IP address to look for.
   * @return An AuthUser instance or <strong>null</strong>.
   */
  
  public synchronized AuthUser lookupUser (InetAddress ipaddr) {
    if ( ipmatcher == null )
      acquireRealm() ;
    return (AuthUser) ipmatcher.lookup(ipaddr.getAddress()) ;
  }
  
  /**
   * Lookup a user by its name.
   * @param name The user's name.
   * @return An AuthUser instance, or <strong>null</strong>.
   */
  
  public synchronized AuthUser lookupUser (String name) {
    if ( realm == null )
      acquireRealm() ;
    return (AuthUser) realm.loadUser(name) ;
  }
  
  /**
   *Checks the timestamp of the nonce to see whether or not its valid.
   *@param date The current date.
   *@param Nonce_date The nonce's date.
   *@return boolean specifying whether the nonce's date was within time limits.
   */  

  public boolean checkTime(Date date, Date Nonce_date)
  {
    long totaltime_nonce = Nonce_date.getTime();
    long totaltime_current = date.getTime();
    if ( (totaltime_current - totaltime_nonce) < 60000)
      {return true;
      }
    else 
      {return false;
      }    
  }
  
  /**
   *Checks the clients nonce to see whether or not it is the correct one from that client/user.
   *@param ctxt The Digest auth context
   *@param ip The client's IP address.
   *@return boolean whether or not the nonce succeeded.
   */
  
  public boolean checkNonce(DigestAuthContext ctxt, InetAddress ip)
  {
    Date current_time = new Date();
    String nonce = ctxt.nonce;
    
    Vector allowed_nonces = getNonces();
    if (allowed_nonces == null)
      {return false;
      }
    else
      {
	int i = 0;
	while (i < allowed_nonces.size())
	  {
	    Vector current_nonce = (Vector) allowed_nonces.elementAt(i);
	    String Vector_nonce = (String) current_nonce.elementAt(0);
	    Date Vector_date = (Date) current_nonce.elementAt(1);
	    InetAddress Vector_ip = (InetAddress) current_nonce.elementAt(2);
	    
	    if (nonce.equals(Vector_nonce)){
	      if (ip.equals(Vector_ip)){
		if (checkTime(current_time, Vector_date))
		  { 
		    return true;
		  }
		else 
		  {
		    allowed_nonces.removeElementAt(i);
		    setValue(ATTR_ALLOWED_NONCES, allowed_nonces);
		  }
	      }
	    }
	    i++;
	  }
	return false;
      }
  }
  
  /**
   * Check the given Digest context against our database.
   * @param ctxt The Digest auth context to check.
   * @param method The method used to access this resource.
   * @return A AuthUser instance if check succeeded, <strong>null</strong>
   *    otherwise.
   */
  
  protected AuthUser checkDigestAuth  (DigestAuthContext ctxt, String method) 
  {
    AuthUser user = lookupUser(ctxt.user) ;
    // This user doesn't even exists !
    if ( user == null )
      {return null ;}
    
    // If it has a password check it
    String password = "";
    if (  user.definesAttribute("password") ) 
      {
	password = user.getPassword();
      }
    
    //Construct response on server to see if they match
    Md5 A1 = new Md5(ctxt.user + ":" + ctxt.realm + ":" + password);
    Md5 A2 = new Md5(method + ":" + ctxt.uri);
    A1.processString();
    A2.processString();
    String HA1 = A1.getStringDigest();
    String HA2 = A2.getStringDigest();
    
    Md5 final_response_string = new Md5(HA1 + ":" + ctxt.nonce + ":" + HA2);
    final_response_string.processString();
    if (ctxt.response.equals(final_response_string.getStringDigest()))
      {
	return user;
      }
    else
      {
	return null; 
      }
    
  }
  
  
  /**
   * Is this user allowed in the realm ?
   * First check in the list of allowed users (if any), than in the list
   * of allowed groups (if any). If no allowed users or allowed groups
   * are defined, than simply check for the existence of this user.
   * @return A boolean <strong>true</strong> if access allowed.
   */
  
  protected boolean checkUser(AuthUser user) {
    String allowed_users[] = getAllowedUsers() ;
    // Check in the list of allowed users:
    if ( allowed_users != null ) {
      for (int i = 0 ; i < allowed_users.length ; i++) {
	if (allowed_users[i].equals(user.getName()))
	  return true ;
      }
    }
    // Check in the list of allowed groups:
    String allowed_groups[] = getAllowedGroups() ;
    if ( allowed_groups != null ) {
      String ugroups[] = user.getGroups() ;
      if ( ugroups != null ) {
	for (int i = 0 ; i < ugroups.length ; i++) {
	  for (int j = 0 ; j < allowed_groups.length ; j++) {
	    if ( allowed_groups[j].equals(ugroups[i]) ) 
	      return true ;
	  }
	}
      }
    }
    // If no users or groups specified, return true
    if ((allowed_users == null) && (allowed_groups == null)) 
      return true ;
    return false ;
  }
  
  
  /**
   * Authenticate the given request.
   * We first check for valid authentication information. If no 
   * authentication is provided, than we try to map the IP address to some
   * of the ones we know about. If the IP address is not found, we challenge
   * the client for a password.
   * <p>If the IP address is found, than either our user entry requires an
   * extra password step (in wich case we challenge it), or simple IP
   * based authentication is enough, so we allow the request.
   * @param request The request to be authentified.
   */
  
  public void authenticate (Request request) 
       throws HTTPException
  {
    // Are we being edited ?
    if ( ! checkRealm() )
      return ;
    // Internal requests always allowed:
    Client client = request.getClient() ;
    if ( client == null )
      return ;
    // Check for User by IP address:
    boolean ipchecked = false ;
    AuthUser user = lookupUser(client.getInetAddress());
    if ( user != null ) {
      ipchecked = true ;
      // Good the user exists, does it need more authentication ?
      if ( ! user.definesAttribute("password") && checkUser(user)) {
	request.defineField("authuser", user.getName()) ;
	request.defineField("authtype", "ip");
	return ;
      }
    }
    
    // Check the basic authentication informations:
    if ( request.hasField("authorization") ) {
      System.out.println("@@@@@@@@@@   it has authorization header!!!!");
      DigestAuthContext ctxt = null ;
      try 
	{
	  ctxt = new DigestAuthContext(request) ;
	} 
      catch (DigestAuthContextException ex) 
	{
	ctxt = null ;
	}
      
      // Is that user allowed ?
      
      if ( ctxt != null ) {      
	user = checkDigestAuth(ctxt, request.getMethod()) ;
	if (checkUser(user) && (user != null) && checkNonce(ctxt, client.getInetAddress())) {
	  // Check that if IP auth was required, it succeeded:
	  boolean iprequired = user.definesAttribute("ipaddress") ;
	  if ( ( ! iprequired) || ipchecked ) {
	    // Set the request fields, and continue:
	    request.defineField("authuser", ctxt.user);
	    request.defineField("authtype", "Digest") ;
	    return ;
	  }
	}
      }	
    }
    
    // Every possible scheme has failed for this request, emit an error
    Reply         e = request.makeReply(HTTP.UNAUTHORISED);
    HtmlGenerator g = new HtmlGenerator("Unauthorised");
    e.setWWWAuthenticate ("Digest", "realm=\"" + getRealm() + "\"," + "nonce=\"" + createNonce(request) + "\"," + "algorithm=\"" + "\"Md5\""     ) ;
    g.append ("<h1>Unauthorised access</h1>"
	      + "<p>You are denied access to this resource.");
    e.setStream(g);
    throw new HTTPException (e);
  }
  
  /**
   * Initialize the filter.
   */
  
  public void initialize(Object values[]) {
    super.initialize(values) ;
  }
  
}


