/*
 * Filename: PcdAuth.java
 *
 * Author: Joe Wang
 *
 * Copyright (c) 2003-2005 
 * The Trustees of Columbia University in the City of New York
 * Columbia University Information Technology
 *
 * This file is part of NINJa.
 *
 * NINJa is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * NINJa is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NINJa; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

package edu.columbia.print.ninja.prix.authenticate;

import com.millipad.util.*;
import java.util.*;
import java.security.Principal;
import javax.security.auth.login.LoginContext;
import javax.print.attribute.standard.*;
import javax.print.attribute.*;
import javax.security.auth.callback.*;
import java.io.*;
import java.net.*;
import java.util.regex.*;

/**
 * PcdAuth authenticates against Kerberos 5 and PCD1/2.  Kerberos 5
 * is used for user verification and password verification and requires
 * that Kerberos 5 be properly installed on the client machine to work.
 * PCD is a printer control daemon implemented by Columbia to perform
 * accounting of printing.  PcdAuth uses the PCD to query the number
 * of pages available and also to deduct from the number of pages available.
 * @author Joe Wang <jw168@columbia.edu>
 * @since 1.0
 */
public class PcdAuth implements edu.columbia.print.ninja.core.authenticate.Authenticator, CallbackHandler, java.io.Serializable {
    
    static final int MAX_LEN = 30;
    static long sequence = 0;
    static java.util.Map tickets = new java.util.HashMap();
    static BlockingStack deductions;

    static {
        try {
            LoginContext lc = null;
            lc = new LoginContext("Prix", new CallbackHandler() {
                public void handle(Callback[] cbs) throws IOException,UnsupportedCallbackException {}
            });
            lc.login();
            lc.logout();
        }
        catch(Exception e) {
        }
    }

    boolean cancelled;
    URI uri;
    String hostName;
    String user, pass;
    int attempts, maxPasswordLength, pageFactor, totalSheets;
    long startTime = -1;
    transient CallbackHandler cbh = null;
    Pattern uniPat;
    char[] lastPass;

    /**
     * @param uri The URI for pcd, the accepted protocols are pcd and pcd2
     * @param pattern The regular expression that determines if the user will be locked out.
     *        If someone sends a job and it shows up as a Columbia uni, this is used to prevent
     *        other people from stealing their job.
     * @param maxPass Setting a maxPass value greater than -1 will cause password truncation
     *        to the length indicated by maxPass.  This is primarily to support password
     *        systems that only allow 8 character passwords (but the user is unaware of this
     *        limitation)
     */
    public PcdAuth(URI uri, String pattern, int maxPass, int pageFactor, int totalSheets) throws IOException {
        this.attempts = 0;
        this.uri = uri;
        this.pageFactor = pageFactor;
        this.totalSheets = totalSheets;
        uniPat = Pattern.compile(pattern);
        
        try {
            hostName = java.net.InetAddress.getLocalHost().getHostName();
        }
        catch(UnknownHostException e) {
            hostName = "Unknown";
        }
        maxPasswordLength = maxPass;
    }
    
    void setURI(URI uri) {this.uri=uri;}
    void setUniPattern(String pattern) {
        if(pattern==null) {
            uniPat = null;
            return;
        }
        uniPat = Pattern.compile(pattern);
    }
    void setMaxPasswordLength(int maxPasswordLength) {this.maxPasswordLength=maxPasswordLength;}
    void setPageFactor(int pageFactor) {this.pageFactor=pageFactor;}
    
    public String toString() {
        return "PcdAuth: "+uri;
    }

    /**
     * Forward callbacks to the CallbackHandler specified in preauthenticate.
     * This also performs an password truncation if necessary.
     * @see PcdAuth#preauthenticate
     */
    public void handle(Callback[] callbacks)
    throws IOException,UnsupportedCallbackException {
        cancelled = false;
        long time = -System.currentTimeMillis();
        try {
            if(cbh==null) {
                debug("unhandled callbacks: no handle");
                return;
            }
            //pre mod
            for(int i=callbacks.length-1; i>=0; i--) {
                if(callbacks[i] instanceof NameCallback) {
                    NameCallback ncb = (NameCallback )callbacks[i];
                    ncb.setName(user);
                }
            }
            cbh.handle(callbacks);
            //post mod
            //clip the passwords
            for(int i=callbacks.length-1; i>=0; i--) {
                if(callbacks[i] instanceof PasswordCallback) {
                    if(maxPasswordLength<0) continue;
                    PasswordCallback pc = (PasswordCallback )callbacks[i];
                    char[] pass = pc.getPassword();
                    lastPass = pass;
                    if(pass.length>maxPasswordLength) {
                        char[] trunc = new char[maxPasswordLength];
                        System.arraycopy(pass, 0, trunc, 0, trunc.length);
                        pc.setPassword(trunc);
                    }
                }
                if(callbacks[i] instanceof NameCallback) {
                    NameCallback nc = (NameCallback )callbacks[i];
                    user = nc.getName();
                }
            }
        }
        catch(UnsupportedCallbackException e) {
            cancelled = true;
            throw e;
        }
    }
    
    static synchronized Long nextSequence() {
        return new Long(sequence++);
    }
    
    static void debug(Object o) {com.millipad.util.Debug.print(PcdAuth.class, o);}
    
    String getUser(LoginContext lc) {
        String user = null;
        try {
            java.util.Set principals = lc.getSubject().getPrincipals(
            javax.security.auth.kerberos.KerberosPrincipal.class
            );
            for(java.util.Iterator j=principals.iterator(); j.hasNext();) {
                Principal pal = (Principal )j.next();
                user = pal.getName();
                int index = user.indexOf("@");
                if(index>=0) user = user.substring(0, index);
            }
        }
        catch(Exception e) {
        }
        return user;
    }
    
    /**
     * Request a Kerberos 5 login from the server, then query pcd for the
     * number of pages available to the user.  Extract the uni from the RequestingUserName
     * attribute.  Test the uni against the lockout pattern.  Attempt a Kerb5 login or
     * bypass if this is a career services printer.  Test for pages against pcd or pcd2.
     */
    public int[] preauthenticate(javax.print.attribute.DocAttributeSet[] docs, net.jini.print.job.PrintRequest[] reqs, boolean consume, edu.columbia.print.ninja.core.authenticate.AuthStub stub, CallbackHandler cbh, Object[] cookies) throws Exception {
        setCallbackHandler(cbh);
        long time = -System.currentTimeMillis();
        //kerb login
        
        user = null;
        RequestingUserName requester;
        requester = (RequestingUserName )((edu.columbia.print.ninja.core.service.RemoteDocPrintRequest )reqs[0]).getAttributes().get(RequestingUserName.class);
        String tmpStr = requester+"";
        int index = tmpStr.indexOf("@");
        if(index>=0) requester = new RequestingUserName(tmpStr.substring(0, index), null);
        if(requester!=null && uniPat!=null) {
            //don't lock out vanity ids
            if(uniPat.matcher(requester+"").matches()) user=requester+"";
        }
        boolean valid = false;
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        for(int i=5; i>0; i--) {	//5 attempts to login
            LoginContext lc = null;
            try {
                lc = new LoginContext("Prix", this);
                //renew lease
                lc.login();
                user = getUser(lc);
                lc.logout();
                valid = true;
                break;
            }
            catch(Exception e) {
                synchronized(this) {
                    if(cancelled) {
                        cancelled = false;
                        throw new Exception("user cancelled request");
                    }
                }
                if("alumprnt".equals(user)
                && "print".equals(String.copyValueOf(lastPass))
                && "ccsa-ninja.atg.columbia.edu".equals(hostName)) {
                    valid = true;
                    break;
                }
                TextOutputCallback t = null;
                String msg = e.getMessage();
                if(msg.indexOf("(6)")>=0) {
                    msg = "Invalid user id";
                    user = null;
                }
                else if(msg.indexOf("(24)")>=0) msg = "Invalid password";
                else if(msg.indexOf("(31)")>=0) msg = "Invalid password";                
                else debug(e);
                t = new TextOutputCallback(
                TextOutputCallback.INFORMATION,
                msg
                );
                cbh.handle(new Callback[] {t});
            }
        }
        
        if(!valid) {
/*                t = new TextOutputCallback(
                        TextOutputCallback.INFORMATION,
                        msg
                        );
                cbh.handle(new Callback[] {t});
 */                    return null;
        }
        //pcd available pages
        
        int total = 0;
        Vector good = new Vector();
        for(int i=reqs.length-1; i>=0; i--) {
            javax.print.attribute.PrintRequestAttributeSet reqSet = reqs[i].getAttributes();
//            reqSet.add(new RequestingUserName(user, null));
            //record cookie
            Sides duplex = (docs[i]==null)? null : (Sides )docs[i].get(Sides.class);
            
            JobMediaSheets sheets = (JobMediaSheets )reqSet.get(JobMediaSheets.class);
            //no JobMediaSheets info!!!
            if(sheets==null) continue;
            total += sheets.getValue();
            //request duplex printing
            cookies[i] = new Object[] {user, duplex, sheets, null};
            
            reqs[i].setAttributes(reqSet);
            good.add(new Integer(i));
        }
        if(good.size()==0) {
            cbh.handle(new Callback[] {
                new TextOutputCallback(
                TextOutputCallback.ERROR,
                "Unable to Print Document(s)"
                )
            });
            return null;
        }
        if(good.size()>1 && totalSheets>0 && total>totalSheets) {
            cbh.handle(new Callback[] {
                new TextOutputCallback(
                TextOutputCallback.ERROR,
                "Please select fewer documents"
                )
            });
            return null;
        }
        total *= pageFactor;
        String noPages = null;
        try {
            if(uri.getScheme().equals("pcd")) {
                int available = -1;
                String cmd = "query("+user+", 3)";
                String cmdResult = pcdCmd(cmd, uri);
                available = Integer.parseInt(cmdResult);
                if(available<total || available<1) noPages = "You do not have enough pages in your quota";
            }
            else if(uri.getScheme().equals("pcd2")) {
                String cmd = "QUERY:"+user+":"+total;
                String cmdResult = pcdCmd(cmd, uri);
//                noPages = cmdResult[1];
                if(Integer.parseInt(cmdResult)!=1) noPages = "You do not have enough pages in your quota";
            }
            else {
                cbh.handle(new Callback[] {
                    new TextOutputCallback(
                    TextOutputCallback.ERROR,
                    "Unknown protocol "+uri.getScheme()
                    )
                });
                return null;
            }
        }
        catch(UnknownHostException e) {
            cbh.handle(new Callback[] {
                new TextOutputCallback(
                TextOutputCallback.ERROR,
                "Unknown pcd host"
                )
            });
            return null;
        }
        catch(java.net.ConnectException e) {
            cbh.handle(new Callback[] {
                new TextOutputCallback(
                TextOutputCallback.ERROR,
                "Failed to contact pcd."
                )
            });
            return null;
        }

        if(noPages!=null) {
            cbh.handle(new Callback[] {
                new TextOutputCallback(TextOutputCallback.ERROR, noPages)
            });
            return null;
        }
        int[] result = new int[good.size()];
        for(int i=good.size()-1; i>=0; i--) {
            Integer tmp = (Integer )good.get(i);
            result[i] = tmp.intValue();
        }
        return result;
    }
    
    static String pcdCmd(String cmd, URI uri) throws UnknownHostException, ConnectException {
        String result = null;
        debug(cmd);
        long time = -System.currentTimeMillis();
        try {
            Socket sock = new Socket(uri.getHost(), uri.getPort());
            java.io.InputStream is = sock.getInputStream();
            java.io.OutputStream os = sock.getOutputStream();
            os.write((cmd+"\n").getBytes());
            os.flush();
            if(uri.getScheme().equals("pcd")) {
                BufferedReader reader = new BufferedReader(
                new java.io.InputStreamReader(is)
                );
                result = reader.readLine();
            }
            else if(uri.getScheme().equals("pcd2")) {
                int value = is.read();
                if(value<0) result="0";
                result = new String(new byte[] {(byte )value});
            }
            else System.out.println("unknown scheme: "+uri+" "+cmd);
            sock.close();
        }
        catch(UnknownHostException e) {
            throw e;
        }
        catch(ConnectException e) {
            throw e;
        }
        catch(IOException e) {
            e.printStackTrace();
        }
        return result;
    }
    
    void setCallbackHandler(CallbackHandler cbh) {this.cbh=cbh;}
    
    public boolean authenticate(Object cookie) {
        String user = (String )((Object[] )cookie)[0];
        Sides sides = (Sides )((Object[] )cookie)[1];
        JobMediaSheets sheets = (JobMediaSheets )((Object[] )cookie)[2];
        int total = sheets.getValue()*pageFactor;
        try {
            if(uri.getScheme().equals("pcd")) {
                int available = -1;
                String cmd = "query("+user+", 3)";
                String cmdResult = pcdCmd(cmd, uri);
                available = Integer.parseInt(cmdResult);
                if(available<total || available<1) return false;
            }
            else if(uri.getScheme().equals("pcd2")) {
                String cmd = "QUERY:"+user+":"+total;
                String cmdResult = pcdCmd(cmd, uri);
    //                noPages = cmdResult[1];
                if(Integer.parseInt(cmdResult)!=1) return false;
            }
        }
        catch(Exception e) {
            return false;
        }
        return true;
    }

    /**
     * Make a call to pcd to deduct the number of printed pages
     * from the number of pages available.
     */
    public void postauthenticate(javax.print.attribute.PrintJobAttributeSet attrs, Object cookie) {
        if(cookie==null) return;
        synchronized(getClass()) {
            if(deductions==null) {
                deductions = new BlockingStack();
                new Thread(new Deducter(deductions)).start();
            }
            JobMediaSheetsCompleted sheets = (JobMediaSheetsCompleted )attrs.get(JobMediaSheetsCompleted.class);
            Sides sides = (Sides )((Object[] )cookie)[1];
            if(sheets==null) {
                System.out.println("no JobMediaSheetsCompleted info");
                return;
            }
            if(attrs.containsValue(JobState.ABORTED)) {
                System.out.println("DEDUCT::0");
                return;
            }
            String duplex;
            if(sides!=null && !sides.equals(Sides.ONE_SIDED)) duplex = "1";
            else duplex = "0";
            int pages = sheets.getValue()*pageFactor;
            int remaining = -1;
            String cmd = "";
            if(uri.getScheme().equals("pcd")) {
                cmd = "deduct("+((Object[] )cookie)[0]+", "+pages+", "+hostName+", "+duplex+")";
            }
            else if(uri.getScheme().equals("pcd2")) {
                cmd = "DEDUCT:"+((Object[] )cookie)[0]+":"+pages;
            }
            else {
                debug("unknown scheme "+uri);
            }
            deductions.push(new Object[] {cmd,uri});
            //debug(cookie+" remaining "+remaining);
        }
    }
    
    static class Deducter implements Runnable {
        BlockingStack stack;
        Deducter(BlockingStack stack) {this.stack=stack;}
        public void run() {
            for(;;) {
                Object[] v = (Object[] )stack.pop();
                String cmd = (String )v[0];
                URI uri = (URI )v[1];
                try {
                    String result = pcdCmd(cmd, uri);
                }
                catch(Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

