
import java.io.*;
import java.util.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//-----------------------------------------------------------------------------
/** 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.
 *
 * @version $Revision: 1.4 $
 * @author Julian Gosnell
 */
class Code { 
   private static final boolean _debugFlag = false;
   
   public static void debug(String msg) {
      if(_debugFlag)
         System.out.println("["+Thread.currentThread().getName()+"]"+msg);             
   }
   public static void debug(String msg, String msg2) {
      if(_debugFlag)
         System.out.println("["+Thread.currentThread().getName()+"]"+msg2+":"+msg);             
   }

   public static void warning(String msg) {
      if(_debugFlag)
         System.out.println("["+Thread.currentThread().getName()+"]"+msg);             
   }
   
   public static void ignore(Exception e) {
      if(_debugFlag)
         e.printStackTrace();
   }
}

class StringUtil {
   public static String nonNull(String val) {
      if(val == null)
         return "";
      return val;
   }
}

public class CGI extends HttpServlet
{
    protected File _docRoot;
    protected String _path;
    protected String _phpCGI;
    
    /* ------------------------------------------------------------ */
    public void
        init()
        throws ServletException
    {


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

        Code.debug("CGI: CGI bin "+tmp);

        _phpCGI = getInitParameter("phpCGI");

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

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

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

        if (!dir.isDirectory())
        {
            Code.warning("CGI: CGI bin is not a directory - "+dir);
            throw new ServletException();
        }
    
        try
        {
            _docRoot=dir.getCanonicalFile();
            Code.debug("CGI: CGI bin accepted - "+_docRoot);
        }
        catch (IOException e)
        {
            Code.warning("CGI: CGI bin failed - "+dir);
            e.printStackTrace();
            throw new ServletException();
        }

        _path=getInitParameter("Path");
        Code.debug("CGI: PATH accepted - "+_path);
    }

    /* ------------------------------------------------------------ */
    public void service(HttpServletRequest req, HttpServletResponse res) 
        throws ServletException, IOException
    {
        Code.debug("CGI: req.getContextPath() : ",req.getContextPath());
        Code.debug("CGI: req.getServletPath() : ",req.getServletPath());
        Code.debug("CGI: req.getPathInfo()    : ",req.getPathInfo());

        Code.debug("CGI: System.Properties : " + System.getProperties().toString());

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

        // what about dos - '\'...should we use pathSeparator ?

        String uri = req.getRequestURI();
        if (uri.indexOf("..") >= 0) {
            res.sendError(404);
            return;
        }
        
        File exe = new File(_docRoot, uri);

        if (!exe.exists()) {
            res.sendError(404);
            return;
        }

        exe = exe.getCanonicalFile();

        Code.debug("CGI: script is "+exe);
        Code.debug("CGI: pathInfo is "+
                   req.getPathInfo());

        exec(exe.toString(), "", req, res);
    }
    
    /* ------------------------------------------------------------ */
    /* 
     * @param root 
     * @param path 
     * @param req 
     * @param res 
     * @exception IOException 
     */
    private void exec(String path,
                      String pathInfo,
                      HttpServletRequest req,
                      HttpServletResponse res)
        throws IOException
    {
        Code.debug("CGI: execing : "+path);
        String 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

            "AUTH_TYPE="                + StringUtil.nonNull(req.getAuthType()),
            "CONTENT_LENGTH="           + (req.getContentLength()<0?0:req.getContentLength()),
            "CONTENT_TYPE="             + StringUtil.nonNull(req.getContentType()),
            "GATEWAY_INTERFACE="        + "CGI/1.1",
            "PATH_INFO="                + StringUtil.nonNull(pathInfo),
            "PATH_TRANSLATED="          + StringUtil.nonNull(req.getPathTranslated()),
            "QUERY_STRING="             + StringUtil.nonNull(req.getQueryString()),
            "REMOTE_ADDR="              + req.getRemoteAddr(),
            "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",

            "REMOTE_USER="              + StringUtil.nonNull(req.getRemoteUser()),
            "REQUEST_METHOD="           + req.getMethod(),
            "SCRIPT_NAME="              + req.getRequestURI().substring(0, req.getRequestURI().length() - pathInfo.length()),
            "SERVER_NAME="              + req.getServerName(),
            "SERVER_PORT="              + req.getServerPort(),
            "SERVER_PROTOCOL="          + req.getProtocol(),
            "SERVER_SOFTWARE="          + getServletContext().getServerInfo(),
            "HTTP_ACCEPT="              + StringUtil.nonNull(req.getHeader("Accept")),
            "HTTP_ACCEPT_CHARSET="      + StringUtil.nonNull(req.getHeader("Accept-Charset")),
            "HTTP_ACCEPT_ENCODING="     + StringUtil.nonNull(req.getHeader("Accept-Encoding")),
            "HTTP_ACCEPT_LANGUAGE="     + StringUtil.nonNull(req.getHeader("Accept-Language")),
            "HTTP_FORWARDED="           + StringUtil.nonNull(req.getHeader("Forwarded")),
            "HTTP_HOST="                + StringUtil.nonNull(req.getHeader("Host")),
            "HTTP_PROXY_AUTHORIZATION=" + StringUtil.nonNull(req.getHeader("Proxy-Authorization")),
            "HTTP_REFERRER="            + StringUtil.nonNull(req.getHeader("Referer")),
            "HTTP_USER_AGENT="          + StringUtil.nonNull(req.getHeader("User-Agent")),
        
            // found these 2 extra headers in request from Jetty - should
            // they be included ?
            "HTTP_PRAGMA="              + StringUtil.nonNull(req.getHeader("Pragma")),
            "HTTP_COOKIE="              + StringUtil.nonNull(req.getHeader("Cookie")),

            // these extra ones were from printenv on www.dev.nomura.co.uk
            "HTTPS="                    + (req.isSecure()?"ON":"OFF"),
            "PATH="                     + _path,
            //       "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...
        Process p;
        if(path.endsWith(".php"))
           p=Runtime.getRuntime().exec(new String[]{_phpCGI, path}, env);
        else
           p=Runtime.getRuntime().exec(path,env);
        
        // 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{
                       int ch;
                       for(int i = 0; i < inputLength; i++) {
                          ch = inFromReq.read();
                          outToCgi.write(ch);
                       }
                        outToCgi.close();
                    }
                    catch(IOException e){Code.ignore(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
            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String line;
            LinkedList list = new LinkedList();
            while((line = br.readLine()) != null) {
               if(line.compareTo("") == 0)
                  break;
               list.add(line);
            }

            String ContentStatus = "Status";
            String location = _headerValue(list, "Content-Location");
            String status   = _headerValue(list, ContentStatus);
            String ct = _headerValue(list, "Content-Type");
            if(ct != null)
               res.setContentType(ct);

            if (status!=null)
            {
                Code.debug("Found a Status header - setting status on response");
                Iterator it = list.iterator();
                while(it.hasNext()) {
                   line = it.next().toString();
                   if(line.startsWith(ContentStatus)) {
                      it.remove();
                      break;
                   }
                }

                // 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...
            Iterator it = list.iterator();
            while(it.hasNext()) {
               line = it.next().toString();
               int index = line.indexOf(':');
               if(index >= 0) {
                  String key = line.substring(0, index);
                  index++;
                  while(index < line.length() 
                        && Character.isWhitespace(line.charAt(index)))
                     index++;
                  String val = line.substring(index);
                  if(!key.equalsIgnoreCase("Content-type")) {
                     res.setHeader(key,val);
                  }
               }
            }

            // copy remains of input onto output...
            PrintWriter out = new PrintWriter(new OutputStreamWriter(res.getOutputStream()));
            char buf[] = new char[7*1024];
            int n;
            while((n = br.read(buf)) > -1) {
               out.write(buf, 0, n);
               out.flush();
            }
        }
        catch (IOException e)
        {
            // browser has closed its input stream - we should
            // terminate script and clean up...
            Code.debug("CGI: Client closed connection!");
            p.destroy();
        }
    }
    
    private String _headerValue(LinkedList headerList, String headerName) {
      Iterator it = headerList.iterator();
      String header;
      int i;
      String name;
      String ret = null;
      while(it.hasNext()) {
         header = it.next().toString();
         i = header.indexOf(':');
         if(i >= 0) {
            name = header.substring(0, i);
            if(name.equalsIgnoreCase(headerName)) {
               i++;
               while(i < header.length() && Character.isWhitespace(header.charAt(i)))
                  i++;
               if(i < header.length())
                  ret = header.substring(i);
               else
                  ret = "";
               break;
            }
         }
      }
      
      return ret;
   }
};

//-----------------------------------------------------------------------------
