Re: CGI script for jigsaw 2

On Tue, 10 Mar 1998, Malcolm McIlhagga wrote:

> I have recently installed Jigsaw 2 on my sun machine (Solaris 2.6,
> jdk1.1.5)
> looking at
> http://lists.w3.org/Archives/Public/www-jigsaw/1998MarApr/0002.html it
> tells me that I should have CGIFrame for 2.0alpha, but resource class
> org.w3c.jigsaw.frames.CGIFrame is not in the packages under the Jigsaw
> installation. where
> can I get this class from and is that all I need to make my perl script
> work?

This frame was not yet ported when the first release (alpha1) went out.
We plan to have a new release soon, as almost everything is now ported to
2.0 (the latest is the Proxy, yesterday :) ).
The source code of CgiFrame
(org.w3c.jigsaw.frames.CgiFrame) is attached.
Regards,

      /\          - Yves Lafon - World Wide Web Consortium - 
  /\ /  \                Architecture Domain - Jigsaw
 /  \    \/\    
/    \   /  \   http://www.w3.org/People/Lafon - ylafon@w3.org    
// CgiFrame.java
// $Id: CgiFrame.java,v 1.2 1998/02/13 10:20:43 bmahe 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.MOVED_TEMPORARILY);
      }
      // 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);
	}
      }
    }
  }

}

Received on Tuesday, 10 March 1998 11:58:54 UTC