// ========================================================================
// A very basic CGI Servlet, for use, with Jetty
// (jetty.mortbay.org). It's heading towards CGI/1.1 compliance, but
// still lacks a few features - the basic stuff is here though...
// Copyright 2000 Julian Gosnell <jules_gosnell@yahoo.com> Released
// under the terms of the Jetty Licence.
// ========================================================================

// TODO
// - logging
// - child's stderr
// - exceptions should report to client via sendError()
// - tidy up

package org.mortbay.servlet;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.mortbay.log.LogFactory;
import org.mortbay.http.HttpFields;
import org.mortbay.util.IO;
import org.mortbay.util.LineInput;
import org.mortbay.util.LogSupport;
import org.mortbay.util.StringUtil;

//-----------------------------------------------------------------------------
/** CGI Servlet.
 *
 * The cgi bin directory can be set with the cgibinResourceBase init
 * parameter or it will default to the resource base of the context.
 *
 * The "commandPrefix" init parameter may be used to set a prefix to all
 * commands passed to exec. This can be used on systems that need assistance
 * to execute a particular file type. For example on windows this can be set
 * to "perl" so that perl scripts are executed.
 *
 * The "Path" init param is passed to the exec environment as PATH.
 * Note: Must be run unpacked somewhere in the filesystem.
 *
 * Any initParameter that starts with ENV_ is used to set an environment
 * variable with the name stripped of the leading ENV_ and using the init
 * parameter value.
 *
 * @version $Revision: 1.27 $
 * @author Julian Gosnell
 */
public class CGI extends HttpServlet
{
    private static Log log = LogFactory.getLog(CGI.class);

    protected File _docRoot;
    protected String _path;
    protected String _cmdPrefix;
    protected EnvList _env;

    /* ------------------------------------------------------------ */
    public void init()
        throws ServletException
    {
        _env= new EnvList();
        _cmdPrefix=getInitParameter("commandPrefix");

        String tmp = getInitParameter("cgibinResourceBase");
        if (tmp==null)
            tmp = getServletContext().getRealPath("/");

        if(log.isDebugEnabled())log.debug("CGI: CGI bin "+tmp);

        if (tmp==null)
        {
            log.warn("CGI: no CGI bin !");
            throw new ServletException();
        }

        File dir = new File(tmp);
        if (!dir.exists())
        {
            log.warn("CGI: CGI bin does not exist - "+dir);
            throw new ServletException();
        }

        if (!dir.canRead())
        {
            log.warn("CGI: CGI bin is not readable - "+dir);
            throw new ServletException();
        }

        if (!dir.isDirectory())
        {
            log.warn("CGI: CGI bin is not a directory - "+dir);
            throw new ServletException();
        }

        try
        {
            _docRoot=dir.getCanonicalFile();
            if(log.isDebugEnabled())log.debug("CGI: CGI bin accepted - "+_docRoot);
        }
        catch (IOException e)
        {
            log.warn("CGI: CGI bin failed - "+dir);
            e.printStackTrace();
            throw new ServletException();
        }

        _path=getInitParameter("Path");
        if(log.isDebugEnabled())log.debug("CGI: PATH accepted - "+_path);
        if (_path != null)
            _env.set("PATH", _path);

        Enumeration e= getInitParameterNames();
        while (e.hasMoreElements())
        {
            String n= (String)e.nextElement();
            if (n != null && n.startsWith("ENV_"))
                _env.set(n.substring(4),getInitParameter(n));
        }
    }

    /* ------------------------------------------------------------ */
    public void service(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
	String pathInContext =
	    StringUtil.nonNull(req.getServletPath()) +
	    StringUtil.nonNull(req.getPathInfo());

	if(log.isDebugEnabled())log.debug("CGI: req.getContextPath() : "+req.getContextPath());
        if(log.isDebugEnabled())log.debug("CGI: req.getServletPath() : "+req.getServletPath());
        if(log.isDebugEnabled())log.debug("CGI: req.getPathInfo()    : "+req.getPathInfo());
        if(log.isDebugEnabled())log.debug("CGI: _docRoot             : "+_docRoot);


        // pathInContext may actually comprises scriptName/pathInfo...We will
        // walk backwards up it until we find the script - the rest must
        // be the pathInfo;

        String both=pathInContext;
        String first=both;
        String last="";

        File exe=new File(_docRoot, first);

        while ((first.endsWith("/") || !exe.exists()) && first.length()>=0)
        {
            int index=first.lastIndexOf('/');

            first=first.substring(0, index);
            last=both.substring(index, both.length());
            exe=new File(_docRoot, first);
        }

        if (first.length()==0 ||
            !exe.exists() ||
            !exe.getCanonicalPath().equals(exe.getAbsolutePath()) ||
            exe.isDirectory())
            res.sendError(404);
        else
        {
            if(log.isDebugEnabled())log.debug("CGI: script is "+exe);
            if(log.isDebugEnabled())log.debug("CGI: pathInfo is "+last);

            exec(exe, last, req, res);
        }
    }

    /* ------------------------------------------------------------ */
    /*
     * @param root
     * @param path
     * @param req
     * @param res
     * @exception IOException
     */
    private void exec(File command,
                      String pathInfo,
                      HttpServletRequest req,
                      HttpServletResponse res)
        throws IOException
    {
        String path=command.toString();
        File dir=command.getParentFile();
        if(log.isDebugEnabled())log.debug("CGI: execing: "+path);

	EnvList env = new EnvList(_env);

	// these ones are from "The WWW Common Gateway Interface Version 1.1"
	// look at : http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1
	env.set("AUTH_TYPE", req.getAuthType());
	env.set("CONTENT_LENGTH", Integer.toString(req.getContentLength()));
	env.set("CONTENT_TYPE", req.getContentType());
	env.set("GATEWAY_INTERFACE", "CGI/1.1");
	env.set("PATH_INFO", pathInfo);
	env.set("PATH_TRANSLATED", req.getPathTranslated());
	env.set("QUERY_STRING", req.getQueryString());
	env.set("REMOTE_ADDR", req.getRemoteAddr());
	env.set("REMOTE_HOST", req.getRemoteHost());
	// The identity information reported about the connection by a
	// RFC 1413 [11] request to the remote agent, if
	// available. Servers MAY choose not to support this feature, or
	// not to request the data for efficiency reasons.
	// "REMOTE_IDENT" => "NYI"
	env.set("REMOTE_USER", req.getRemoteUser());
	env.set("REQUEST_METHOD", req.getMethod());
    String scriptName = req.getRequestURI().substring(0,req.getRequestURI().length() - pathInfo.length());
	env.set("SCRIPT_NAME",scriptName);
    env.set("SCRIPT_FILENAME",getServletContext().getRealPath(scriptName));
    env.set("SERVER_NAME", req.getServerName());
	env.set("SERVER_PORT", Integer.toString(req.getServerPort()));
	env.set("SERVER_PROTOCOL", req.getProtocol());
        env.set("SERVER_SOFTWARE", getServletContext().getServerInfo());
	Enumeration enm = req.getHeaderNames();
	while (enm.hasMoreElements())
	{
	    String name = (String) enm.nextElement();
	    String value = req.getHeader(name);
	    env.set("HTTP_" + name.toUpperCase().replace( '-', '_' ), value);
	}

	// these extra ones were from printenv on www.dev.nomura.co.uk
	env.set("HTTPS", (req.isSecure()?"ON":"OFF"));
	// "DOCUMENT_ROOT" => root + "/docs",
	// "SERVER_URL" => "NYI - http://us0245",
	// "TZ" => System.getProperty("user.timezone"),

        // are we meant to decode args here ? or does the script get them
        // via PATH_INFO ?  if we are, they should be decoded and passed
        // into exec here...

        String execCmd=path;
        if (execCmd.indexOf(" ")>=0)
            execCmd="\""+execCmd+"\"";
        if (_cmdPrefix!=null)
            execCmd=_cmdPrefix+" "+execCmd;

        Process p=dir==null
            ?Runtime.getRuntime().exec(execCmd, env.getEnvArray())
            :Runtime.getRuntime().exec(execCmd, env.getEnvArray(),dir);

        // hook processes input to browser's output (async)
        final InputStream inFromReq=req.getInputStream();
        final OutputStream outToCgi=p.getOutputStream();
        final int inputLength = req.getContentLength();

        new Thread(new Runnable()
            {
                public void run()
                {
                    try{
                        if (inputLength>0)
                            IO.copy(inFromReq,outToCgi,inputLength);
                        outToCgi.close();
                    }
                    catch(IOException e){LogSupport.ignore(log,e);}
                }
            }).start();


        // hook processes output to browser's input (sync)
        // if browser closes stream, we should detect it and kill process...
        try
        {
            // read any headers off the top of our input stream
            LineInput li = new LineInput(p.getInputStream());
            HttpFields fields=new HttpFields();
            fields.read(li);

            String ContentStatus = "Status";
            String redirect = fields.get(HttpFields.__Location);
            String status   = fields.get(ContentStatus);

            if (status!=null)
            {
                log.debug("Found a Status header - setting status on response");
                fields.remove(ContentStatus);

                // NOTE: we ignore any reason phrase, otherwise we
                // would need to use res.sendError() selectively.
                int i = status.indexOf(' ');
                if (i>0)
                    status = status.substring(0,i);

                res.setStatus(Integer.parseInt(status));
            }

            // copy remaining headers into response...
	    for (Iterator i=fields.iterator(); i.hasNext();)
            {
                HttpFields.Entry e=(HttpFields.Entry)i.next();
                res.addHeader(e.getKey(),e.getValue());
            }

            if (status==null && redirect != null)
            {
                // The CGI has set Location and is counting on us to do the redirect.
                // See http://CGI-Spec.Golux.Com/draft-coar-cgi-v11-03-clean.html#7.2.1.2
                if (!redirect.startsWith("http:/")&&!redirect.startsWith("https:/"))
                    res.sendRedirect(redirect);
                else
                    res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
            }

            // copy remains of input onto output...
            IO.copy(li, res.getOutputStream());

	    p.waitFor();
	    int exitValue = p.exitValue();
	    if(log.isDebugEnabled())log.debug("CGI: p.exitValue(): " + exitValue);
	    if (0 != exitValue)
	    {
		log.warn("Non-zero exit status ("+exitValue+
			     ") from CGI program: "+path);
		if (!res.isCommitted())
		    res.sendError(500, "Failed to exec CGI");
	    }
        }
        catch (IOException e)
        {
            // browser has probably closed its input stream - we
            // terminate and clean up...
            log.debug("CGI: Client closed connection!");
        }
	catch (InterruptedException ie)
        {
            log.debug("CGI: interrupted!");
        }
	finally
	{
            p.destroy();
	}

	if(log.isDebugEnabled())log.debug("CGI: Finished exec: " + p);
    }


    /* ------------------------------------------------------------ */
    /** private utility class that manages the Environment passed
     * to exec.
     */
    private static class EnvList
    {
        private Map envMap;

        EnvList()
        {
            envMap= new HashMap();
        }

        EnvList(EnvList l)
        {
            envMap= new HashMap(l.envMap);
        }

	/** Set a name/value pair, null values will be treated as
	 * an empty String */
	public void set(String name, String value) {
            envMap.put(name, name + "=" + StringUtil.nonNull(value));
	}

	/** Get representation suitable for passing to exec. */
	public String[] getEnvArray()
	{
            return (String[])envMap.values().toArray(new String[envMap.size()]);
	}
    }
}
