CgiFrame.java
Author: Benoit Mahe

    // CgiFrame.java
    // $Id: CgiFrame.html,v 1.3 1999/10/27 22:10:33 ylafon Exp $
    // (c) COPYRIGHT MIT and INRIA, 1996.
    // Please first read the full copyright statement in file COPYRIGHT.html

    package org.w3c.jigsaw.frames ;

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

    import org.w3c.tools.resources.*;
    import org.w3c.tools.resources.ProtocolException;
    import org.w3c.www.mime.* ;
    import org.w3c.jigsaw.http.* ;
    import org.w3c.www.http.*;
    import org.w3c.jigsaw.auth.AuthFilter;

    /**
     * Parsing the CGI output - The CGIHeaderHolder, to hold CGI headers.
     */
    class CGIHeaderHolder implements MimeHeaderHolder {
        // Status and Location deserve special treatments:
        String status   = null;
        String location = null;
        // Anyway, he is going to pay for using CGI
        Hashtable headers = null;
        // The MIME parse we are attached to:
        MimeParser parser = null;

        /**
         * The parsing is now about to start, take any appropriate action.
         * This hook can return a <strong>true</strong> boolean value to enforce
         * the MIME parser into transparent mode (eg the parser will <em>not</em>
         * try to parse any headers.
         * <p>This hack is primarily defined for HTTP/0.9 support, it might
         * also be usefull for other hacks.
         * @param parser The Mime parser.
         * @return A boolean <strong>true</strong> if the MimeParser shouldn't
         * continue the parsing, <strong>false</strong> otherwise.
         */

        public boolean notifyBeginParsing(MimeParser parser)
    	throws IOException
        {
    	return false;
        }

        /**
         * All the headers have been parsed, take any appropriate actions.
         * @param parser The Mime parser.
         */

        public void notifyEndParsing(MimeParser parser)
    	throws IOException
        {
    	return ;
        }

        /**
         * A new header has been emited by the script.
         * If the script is not an NPH, then it <strong>must</strong> at least
         * emit one header, so we are safe here, although people may not be safe
         * against the spec.
         * @param name The header name.
         * @param buf The header bytes.
         * @param off The begining of the value bytes  in above buffer.
         * @param len The length of the value bytes  in above buffer.
         */

        public void notifyHeader(String name, byte buf[], int off, int len)
    	throws MimeParserException
        {
    	if ( name.equalsIgnoreCase("status") ) {
    	    status = new String(buf, 0, off, len);
    	} else if ( name.equalsIgnoreCase("location") ) {
    	    location = new String(buf, 0, off, len);
    	} else {
    	    String extraval =  new String(buf, 0, off, len);
    	    if ( headers == null ) {
    		headers = new Hashtable(11);
    	    } else {
    		String val = (String) headers.get(name.toLowerCase());
    		if ( val != null )
    		    extraval = val + "," + extraval;
    	    }
    	    headers.put(name.toLowerCase(), extraval);
    	}
        }

        /**
         * Get the status emited by the script.
         */

        public String getStatus() {
    	return status;
        }

        /**
         * Get the location header value emited by the script.
         */

        public String getLocation() {
    	return location;
        }

        /**
         * Get any header value (except status and location).
         * @param name The name of the header to fetch.
         * @return The string value of requested header, or <strong>null</strong>
         * if header was not defined.
         */

        public String getValue(String name) {
    	return (headers == null) ? null : (String) headers.get(name);
        }

        /**
         * Enumerate the headers defined by the holder.
         * @return A enumeration of header names, or <strong>null</strong> if no
         * header is defined.
         */

        public Enumeration enumerateHeaders() {
    	if ( headers == null )
    	    return null;
    	return headers.keys();
        }

        /**
         * Get the remaining output of the stream.
         * This should be called only once header parsing is done.
         */

        public InputStream getInputStream() {
    	return parser.getInputStream();
        }

        CGIHeaderHolder(MimeParser parser) {
    	this.parser = parser;
        }

    }

    /**
     * Parsing the CGI output - Always create a CGIHeaderHolder.
     */

    class CGIHeaderHolderFactory implements MimeParserFactory {

        /**
         * Create a new header holder to hold the parser's result.
         * @param parser The parser that has something to parse.
         * @return A MimeParserHandler compliant object.
         */

        public MimeHeaderHolder createHeaderHolder(MimeParser parser) {
    	return new CGIHeaderHolder(parser);
        }

        CGIHeaderHolderFactory() {
        }

    }

    /**
     * A simple process feeder class.
     */

    class ProcessFeeder extends Thread {
        Process      proc    = null ;
        OutputStream out     = null ;
        InputStream  in      = null ;
        int          count = -1 ;

        public void run () {
    	try {
    	    byte buffer[] = new byte[4096] ;
    	    int  got      = -1 ;

    	    // Send the data to the target process:
    	    if ( count >= 0 ) {
    		while ( (count > 0) && ((got = in.read(buffer)) > 0) ) {
    		    out.write (buffer, 0, got) ;
    		    count -= got ;
    		}
    	    } else {
    		while ( (got = in.read(buffer)) > 0 ) {
    		    out.write (buffer, 0, got) ;
    		}
    	    }
    	} catch (Exception e) {
    	    System.out.println ("ProcessFeeder: caught exception !") ;
    	    e.printStackTrace() ;
    	} finally {
    	    // Clean up the process:
    	    try { out.flush() ; } catch (IOException ex) {}
    	    try { out.close() ; } catch (IOException ex) {}
    	    try { proc.waitFor() ; } catch (Exception ex) {}
    	}
        }

        ProcessFeeder (Process proc, InputStream in) {
    	this (proc, in, -1) ;
        }

        ProcessFeeder (Process proc, InputStream in, int count) {
    	this.proc   = proc ;
    	this.out    = proc.getOutputStream() ;
    	this.in     = in ;
    	this.count  = count ;
        }
    }

    /**
     * Handle CGI scripts.
     */
    public class CgiFrame extends HTTPFrame {

        private final static
    	String STATE_EXTRA_PATH = "org.w3c.jigsaw.frames.CgiFrame.extraPath";

        /**
         * Attribute index - The interpreter to use, if any.
         */
        protected static int ATTR_INTERPRETER = -1;
        /**
         * Attribute index - The array of string that makes the command to run.
         */
        protected static int ATTR_COMMAND = -1 ;
        /**
         * Attribute index - Does the script takes care of its headers ?
         */
        protected static int ATTR_NOHEADER = -1 ;
        /**
         * Attribute index - Does the script generates the form on GET ?
         */
        protected static int ATTR_GENERATES_FORM = -1 ;
        /**
         * Attribute index - Do DNS, to fill in REMOTE_HOST env var.
         */
        protected static int ATTR_REMOTE_HOST = -1;
        /**
         * Attribute index - Turn the script in debug mode.
         */
        protected static int ATTR_CGI_DEBUG = -1;

        static {
    	Attribute a   = null ;
    	Class     cls = null ;
    	try {
    	    cls = Class.forName("org.w3c.jigsaw.frames.CgiFrame") ;
    	} catch (Exception ex) {
    	    ex.printStackTrace() ;
    	    System.exit(1) ;
    	}
    	// The interpreter attribute:
    	a = new StringAttribute("interpreter"
    				, null
    				, Attribute.EDITABLE);
    	ATTR_INTERPRETER = AttributeRegistry.registerAttribute(cls, a);
    	// The command attribute:
    	a = new StringArrayAttribute("command"
    				     , null
    				     , Attribute.MANDATORY|Attribute.EDITABLE);
    	ATTR_COMMAND = AttributeRegistry.registerAttribute(cls, a) ;
    	// The noheader attribute:
    	a = new BooleanAttribute("noheader"
    				 , Boolean.FALSE
    				 , Attribute.EDITABLE) ;
    	ATTR_NOHEADER = AttributeRegistry.registerAttribute(cls, a) ;
    	// The generates form attribute
    	a = new BooleanAttribute("generates-form"
    				 , Boolean.TRUE
    				 , Attribute.EDITABLE) ;
    	ATTR_GENERATES_FORM = AttributeRegistry.registerAttribute(cls, a);
    	// Registerr the DODNS attribute.
    	a = new BooleanAttribute("remote-host"
    				 , null
    				 , Attribute.EDITABLE);
    	ATTR_REMOTE_HOST = AttributeRegistry.registerAttribute(cls, a);
    	// Register the debug mode flag:
    	a = new BooleanAttribute("cgi-debug"
    				 , Boolean.FALSE
    				 , Attribute.EDITABLE);
    	ATTR_CGI_DEBUG = AttributeRegistry.registerAttribute(cls, a);
        }

        /**
         * Get the interpreter to use to execute the script.
         * This is most usefull for operating systems that don't have a
         * <code>!#</code> convention ala UNIX.
         * @return The interpreter to run the script.
         */

        public String getInterpreter() {
    	return getString(ATTR_INTERPRETER, null);
        }

        /**
         * Get the command string array.
         */

        public String[] getCommand() {
    	return (String[]) getValue(ATTR_COMMAND, null) ;
        }

        /**
         * Get the noheader flag.
         * @return The boolean value of the noheader flag.
         */

        public boolean checkNoheaderFlag() {
    	return getBoolean(ATTR_NOHEADER, false) ;
        }

        /**
         * Get the generates form flag.
         * @return The boolean value of the generates form flag.
         */

        public boolean checkGeneratesFormFlag() {
    	return getBoolean(ATTR_GENERATES_FORM, true) ;
        }

        /**
         * Get the remote host attribute value.
         * If turned on, this flag will enable the REMOTE_HOST env var computation.
         * @return A boolean.
         */

        public boolean checkRemoteHost() {
    	return getBoolean(ATTR_REMOTE_HOST, false);
        }

        /**
         * Get the CGI debug flag.
         * @return The boolean value of the CGI debug flag.
         */

        public boolean checkCgiDebug() {
    	return getBoolean(ATTR_CGI_DEBUG, false);
        }

        /**
         * Turn the given header name into it's env var canonical name.
         * This guy is crazy enough to run CGI scripts, he can pay for that
         * overhead.
         * @param name The header name.
         * @return A String giving the official env variable name for that header.
         */

        public String getEnvName(String name) {
    	int          sl = name.length();
    	StringBuffer sb = new StringBuffer(5+sl);
    	sb.append("HTTP_");
    	for (int i = 0 ; i < sl ; i++) {
    	    char ch = name.charAt(i);
    	    sb.append((ch == '-') ? '_' : Character.toUpperCase(ch));
    	}
    	return sb.toString();
        }

        /**
         * Handle the CGI script output.
         * This methods handles the CGI script output. Depending on the
         * value of the <strong>noheader</strong> attribute it either:
         * <ul>
         * <li>Sends back the script output directly,</li>
         * <li>Parses the script output, looking for a status header or a
         * location header, or a content-length header, or any combination
         * of those three.
         * </ul>
         * @param process The underlying CGI process.
         * @param request The processed request.
         * @exception ProtocolException If an HTTP error should be sent back
         * to the client.
         */

        protected Reply handleCGIOutput (Process process, Request request)
    	throws ProtocolException
        {
    	// No header script don't deserve attention:
    	if ( checkNoheaderFlag() ) {
    	    Reply reply = request.makeReply(HTTP.NOHEADER) ;
    	    reply.setStream (process.getInputStream()) ;
    	    return reply ;
    	}
    	// Check for debugging mode:
    	if ( checkCgiDebug() ) {
    	    Reply reply = request.makeReply(HTTP.OK);
    	    reply.setContentType(MimeType.TEXT_PLAIN);
    	    reply.setStream(process.getInputStream());
    	    return reply;
    	}
    	// We MUST parse at least one header:
    	MimeParser p = new MimeParser(process.getInputStream(),
    				      new CGIHeaderHolderFactory());
    	Reply           reply  = null ;
    	try {
    	    CGIHeaderHolder h = (CGIHeaderHolder) p.parse();
    	    // Check for a status code:
    	    String svalue   = h.getStatus();
    	    String location = h.getLocation();
    	    if ( svalue != null ) {
    		int status = -1;
    		try {
    		    status = Integer.parseInt(svalue);
    		} catch (Exception ex) {
    		    // This script has emited an invalid status line:
    		    String msg = ("Emited an invalid status line ["+
    				  svalue + "].");
    		    getServer().errlog(this, msg);
    		    // Throw an HTTPException:
    		    reply = request.makeReply(HTTP.INTERNAL_SERVER_ERROR);
    		    reply.setContent("CGI script emited invalid status.");
    		    throw new HTTPException(reply);
    		}
    		reply = request.makeReply(status);
    	    } else {
    		// No status code available, any location header ?
    		reply = request.makeReply((location == null)
    					  ? HTTP.OK
    					  : HTTP.FOUND);
    	    }
    	    // Set up the location header if needed:
    	    if ( location != null ) {
    		try {
    		    reply.setLocation(new URL(getURL(request), location));
    		} catch (MalformedURLException ex) {
    		    // This should really not happen:
    		    getServer().errlog(this, "unable to create location url "+
    				       location+
    				       " in base "+getURL(request));
    		}
    	    }
    	    // And then, the remaining headers:
    	    Enumeration e = h.enumerateHeaders();
    	    if ( e != null ) {
    		while ( e.hasMoreElements() ) {
    		    String hname = (String) e.nextElement();
    		    reply.setValue(hname, (String) h.getValue(hname));
    		}
    	    }
    	    reply.setStream(p.getInputStream()) ;
    	} catch (IOException ex) {
    	    ex.printStackTrace();
    	} catch (MimeParserException ex) {
    	    // This script has generated invalid output:
    	    String msg = (getURL(request)
    			  +": emited invalid output ["+
    			  ex.getMessage() +"]");
    	    getServer().errlog(this, msg);
    	    // Throw an HTTPException:
    	    Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
    	    error.setContent("CGI error: unable to parse script headers.") ;
    	    throw new HTTPException (error) ;
    	}
    	return reply ;
        }

        /**
         * Add an enviornment binding to the given vector.
         * @param name The name of the enviornment variable.
         * @param val Its value.
         * @param into The vector to which accumulate bindings.
         */

        private void addEnv (String name, String val, Vector into) {
    	into.addElement (name+"="+val) ;
        }

        /**
         * Prepare the command to run for this CGI script, and run it.
         * @param request The request to handle.
         * @return The running CGI process object.
         * @exception HTTPException If we weren't able to build the command or
         *    the environment.
         */

        protected Process makeCgiCommand (Request request)
    	throws ProtocolException, IOException
        {
    	// Check the command attribute first:
    	String      query     = null;
    	String      command[] = getCommand() ;
    	if ( command == null ) {
    	    Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
    	    error.setContent("CgiResource mis-configured: it doesn't have a "
    			     + " command attribute");
    	    throw new HTTPException(error);
    	}
    	// Ok:
    	Vector      env       = new Vector(32) ;
    	httpd       server    = request.getClient().getServer() ;
    	InetAddress sadr      = server.getInetAddress() ;
    	// Specified environment variables:
    	// We do not handle the following variables:
    	// - PATH_TRANSLATED: I don't understand it
    	// - REMOTE_IDENT: would require usage of IDENT protocol.
    	// Authentification type, if any:
    	String svalue = (String) request.getState(AuthFilter.STATE_AUTHTYPE);
    	if ( svalue != null )
    	    addEnv("AUTH_TYPE", svalue, env);
    	// Content length, if available:
    	svalue = request.getValue("content-length");
    	if ( svalue != null )
    	    addEnv("CONTENT_LENGTH", svalue, env);
    	// Content type, if available:
    	svalue = request.getValue("content-type");
    	if ( svalue != null )
    	    addEnv("CONTENT_TYPE", svalue, env);
    	// The gateway interface, hopefully 1.1 !
    	addEnv ("GATEWAY_INTERFACE", "CGI/1.1", env) ;
    	// The PATH_INFO, which I am afraid I still don't understand:
    	svalue = (String) request.getState(STATE_EXTRA_PATH);
    	if ( svalue == null )
    	    addEnv ("PATH_INFO", "/", env) ;
    	else
    	    addEnv ("PATH_INFO", svalue, env) ;
    	// The query string:
    	query = request.getQueryString();
    	if ( query != null )
    	    addEnv("QUERY_STRING", query, env) ;
    	// The remote client IP address:
    	svalue = request.getClient().getInetAddress().toString();
    	addEnv ("REMOTE_ADDR", svalue, env);
    	// Authentified user:
    	svalue = (String) request.getState(AuthFilter.STATE_AUTHUSER);
    	if ( svalue != null )
    	    addEnv("REMOTE_USER", svalue, env);
    	// Remote host name, if allowed:
    	if ( checkRemoteHost() ) {
    	    String host = request.getClient().getInetAddress().getHostName();
    	    addEnv("REMOTE_HOST", host, env);
    	}
    	// The request method:
    	addEnv ("REQUEST_METHOD", request.getMethod(), env) ;
    	// The script name :
    	addEnv("SCRIPT_NAME", getURLPath(), env);
    	// Server name:
    	addEnv ("SERVER_NAME", getServer().getHost(), env) ;
    	// Server port:
    	svalue = Integer.toString(getServer().getLocalPort());
    	addEnv ("SERVER_PORT", svalue, env);
    	// Server protocol:
    	addEnv ("SERVER_PROTOCOL", request.getVersion(), env) ;
    	// Server software:
    	addEnv ("SERVER_SOFTWARE", server.getSoftware(), env) ;
    	// All other request fields, yeah, let's lose even more time:
    	Enumeration e = request.enumerateHeaderDescriptions(false);
    	while ( e.hasMoreElements() ) {
    	    HeaderDescription d = (HeaderDescription) e.nextElement();
    	    addEnv(getEnvName(d.getName())
    		   , request.getHeaderValue(d).toString()
    		   , env);
    	}
    	// Command line:
    	if ( query != null ) {
    	    String querycmd[] = new String[command.length+1] ;
    	    System.arraycopy(command, 0, querycmd, 0, command.length) ;
    	    querycmd[command.length] = query ;
    	    command = querycmd ;
    	}
    	String aenv[] = new String[env.size()] ;
    	env.copyInto (aenv) ;
    	// Run the process:
    	if ( getInterpreter() != null ) {
    	    String run[] = new String[command.length+1];
    	    run[0] = getInterpreter();
    	    System.arraycopy(command, 0, run, 1, command.length);
    	    return Runtime.getRuntime().exec (run, aenv) ;
    	} else {
    	    return Runtime.getRuntime().exec (command, aenv) ;
    	}
        }

        /**
         * Lookup sub-resources.
         * Accumulate the remaning path in some special state of the request.
         * <p>This allows us to implement the <code>PATH_INFO</code>
         * CGI variable properly.
         * @param ls Current lookup state.
         * @param lr Lookup result under construction.
         * @return A boolean <strong>true</strong> if lookup should continue,
         * <strong>false</strong> otherwise.
         */

        public boolean lookup(LookupState ls, LookupResult lr)
    	throws ProtocolException
        {
    	// Get the extra path information:
    	String extraPath = ls.getRemainingPath(true);
    	if ((extraPath == null) || extraPath.equals(""))
    	    extraPath = "/";
    	// Keep this path info into the request, if possible:
    	Request request = (Request) ls.getRequest();
    	if ( request != null )
    	    request.setState(STATE_EXTRA_PATH, extraPath);
    	lr.setTarget(getResource().getResourceReference());
    	return super.lookup(ls, lr);
        }

        /**
         * GET method implementation.
         * this method is splitted into two cases:
         * <p>If the resource is able to generates its form, than run the script
         * to emit the form. Otherwsie, use our super class (FileResource) ability
         * to send the file that contains the form.
         * <p>Note that there is no need to feed the underlying process with
         * data in the GET case.
         * @param request The request to handle.
         * @exception ProtocolException If processing the request failed.
         */
        public Reply get(Request request)
    	throws ProtocolException, NotAProtocolException
       {
           if ( ! checkGeneratesFormFlag() )
    	   return super.get (request) ;
           Process       process = null ;
           try {
    	   process = makeCgiCommand (request) ;
           } catch (IOException e) {
    	   Reply error = request.makeReply(HTTP.NOT_FOUND) ;
    	   error.setContent("The resource's script wasn't found.") ;
    	   throw new HTTPException (error) ;
           }
           return handleCGIOutput (process, request) ;
       }

        /**
         * Handle the POST method according to CGI/1.1 specification.
         * The request body is sent back to the launched CGI script, as is, and
         * the script output is handled by the handleCGIOutput method.
         * @param request The request to process.
         * @exception ProtocolException If the processing failed.
         */
        public Reply post(Request request)
    	throws ProtocolException, NotAProtocolException
        {
    	Process       process = null ;
    	// Launch the CGI process:
    	try {
    	    process = makeCgiCommand(request) ;
    	} catch (IOException ex) {
    	    // The process wasn't executable, emit a errlog message:
    	    String msg = ("The process "+
    			  getCommand()[0] +" couldn't be executed ["+
    			  ex.getMessage() + "]");
    	    getServer().errlog(this, msg);
    	    // Throw an internal server error:
    	    Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
    	    error.setContent("CGI script is misconfigured.");
    	    throw new HTTPException (error) ;
    	}
    	// Now feed the process:
    	try {
    	    // Send the 100 status code:
    	    Client client = request.getClient();
    	    if ( client != null )
    		client.sendContinue();
    	    InputStream in = request.getInputStream();
    	    if ( in == null ) {
    		// There was no input to that CCI, close process stream
    		process.getOutputStream().close();
    	    } else {
    		// Some input to feed the process with:
    		(new ProcessFeeder(process, in)).start();
    	    }
    	} catch (IOException ex) {
    	    // This is most probably a bad request:
    	    Reply error = request.makeReply(HTTP.BAD_REQUEST);
    	    error.setContent("The request didn't have a valid input.");
    	    throw new HTTPException(error);
    	}
    	return handleCGIOutput(process, request);
        }

        /**
         * At register time, if no command, use a suitable default.
         * THis method will set the command to the identifier, if it is not
         * provided.
         * @param values Default attribute values.
         */

        public void registerResource(FramedResource resource) {
    	super.registerResource(resource);
    	//if no command is specified look for a file resource attached
    	//and get its File absolute path if available.
    	if (getCommand() == null) {
    	    if (getFileResource() != null) {
    		if (getFileResource().getFile() != null) {
    		    String cmd[] = new String[1];
    		    cmd[0] = getFileResource().getFile().getAbsolutePath();
    		    setValue(ATTR_COMMAND, cmd);
    		}
    	    }
    	}
        }
    }