View Javadoc

    1   //========================================================================
    2   //Copyright 2006 Mort Bay Consulting Pty. Ltd.
    3   //------------------------------------------------------------------------
    4   //Licensed under the Apache License, Version 2.0 (the "License");
    5   //you may not use this file except in compliance with the License.
    6   //You may obtain a copy of the License at
    7   //http://www.apache.org/licenses/LICENSE-2.0
    8   //Unless required by applicable law or agreed to in writing, software
    9   //distributed under the License is distributed on an "AS IS" BASIS,
    10  //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  //See the License for the specific language governing permissions and
    12  //limitations under the License.
    13  //========================================================================
    14
    15  package org.mortbay.servlet;
    16
    17  import java.io.BufferedReader;
    18  import java.io.File;
    19  import java.io.IOException;
    20  import java.io.InputStream;
    21  import java.io.InputStreamReader;
    22  import java.io.OutputStream;
    23  import java.io.PrintWriter;
    24  import java.util.Enumeration;
    25  import java.util.HashMap;
    26  import java.util.Map;
    27
    28  import javax.servlet.ServletException;
    29  import javax.servlet.http.HttpServlet;
    30  import javax.servlet.http.HttpServletRequest;
    31  import javax.servlet.http.HttpServletResponse;
    32
    33  import org.mortbay.log.Log;
    34  import org.mortbay.util.IO;
    35  import org.mortbay.util.StringUtil;
    36
    37  //-----------------------------------------------------------------------------
    38  /**
    39   * CGI Servlet.
    40   *
    41   * The cgi bin directory can be set with the "cgibinResourceBase" init parameter
    42   * or it will default to the resource base of the context.
    43   *
    44   * The "commandPrefix" init parameter may be used to set a prefix to all
    45   * commands passed to exec. This can be used on systems that need assistance to
    46   * execute a particular file type. For example on windows this can be set to
    47   * "perl" so that perl scripts are executed.
    48   *
    49   * The "Path" init param is passed to the exec environment as PATH. Note: Must
    50   * be run unpacked somewhere in the filesystem.
    51   *
    52   * Any initParameter that starts with ENV_ is used to set an environment
    53   * variable with the name stripped of the leading ENV_ and using the init
    54   * parameter value.
    55   *
    56   * @author Julian Gosnell
    57   * @author Thanassis Papathanasiou - Some minor modifications for Jetty6 port
    58   */
    59  public class CGI extends HttpServlet
    60  {
    61      private boolean _ok;
    62      private File _docRoot;
    63      private String _path;
    64      private String _cmdPrefix;
    65      private EnvList _env;
    66      private boolean _ignoreExitState;
    67
    68      /* ------------------------------------------------------------ */
    69      public void init() throws ServletException
    70      {
    71          _env=new EnvList();
    72          _cmdPrefix=getInitParameter("commandPrefix");
    73
    74          String tmp=getInitParameter("cgibinResourceBase");
    75          if (tmp==null)
    76          {
    77              tmp=getInitParameter("resourceBase");
    78              if (tmp==null)
    79                  tmp=getServletContext().getRealPath("/");
    80          }
    81
    82          if (tmp==null)
    83          {
    84              Log.warn("CGI: no CGI bin !");
    85              return;
    86          }
    87
    88          File dir=new File(tmp);
    89          if (!dir.exists())
    90          {
    91              Log.warn("CGI: CGI bin does not exist - "+dir);
    92              return;
    93          }
    94
    95          if (!dir.canRead())
    96          {
    97              Log.warn("CGI: CGI bin is not readable - "+dir);
    98              return;
    99          }
    100
    101         if (!dir.isDirectory())
    102         {
    103             Log.warn("CGI: CGI bin is not a directory - "+dir);
    104             return;
    105         }
    106
    107         try
    108         {
    109             _docRoot=dir.getCanonicalFile();
    110         }
    111         catch (IOException e)
    112         {
    113             Log.warn("CGI: CGI bin failed - "+dir,e);
    114             return;
    115         }
    116
    117         _path=getInitParameter("Path");
    118         if (_path!=null)
    119             _env.set("PATH",_path);
    120
    121         _ignoreExitState="true".equalsIgnoreCase(getInitParameter("ignoreExitState"));
    122         Enumeration e=getInitParameterNames();
    123         while (e.hasMoreElements())
    124         {
    125             String n=(String)e.nextElement();
    126             if (n!=null&&n.startsWith("ENV_"))
    127                 _env.set(n.substring(4),getInitParameter(n));
    128         }
    129         if(!_env.envMap.containsKey("SystemRoot"))
    130         {
    131       	    String os = System.getProperty("os.name");
    132             if (os!=null && os.toLowerCase().indexOf("windows")!=-1)
    133             {
    134         	String windir = System.getProperty("windir");
    135         	_env.set("SystemRoot", windir!=null ? windir : "C:\\WINDOWS");
    136             }
    137         }
    138
    139         _ok=true;
    140     }
    141
    142     /* ------------------------------------------------------------ */
    143     public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
    144     {
    145         if (!_ok)
    146         {
    147             res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
    148             return;
    149         }
    150
    151         String pathInContext=StringUtil.nonNull(req.getServletPath())+StringUtil.nonNull(req.getPathInfo());
    152
    153         if (Log.isDebugEnabled())
    154         {
    155             Log.debug("CGI: ContextPath : "+req.getContextPath());
    156             Log.debug("CGI: ServletPath : "+req.getServletPath());
    157             Log.debug("CGI: PathInfo    : "+req.getPathInfo());
    158             Log.debug("CGI: _docRoot    : "+_docRoot);
    159             Log.debug("CGI: _path       : "+_path);
    160             Log.debug("CGI: _ignoreExitState: "+_ignoreExitState);
    161         }
    162
    163         // pathInContext may actually comprises scriptName/pathInfo...We will
    164         // walk backwards up it until we find the script - the rest must
    165         // be the pathInfo;
    166
    167         String both=pathInContext;
    168         String first=both;
    169         String last="";
    170
    171         File exe=new File(_docRoot,first);
    172
    173         while ((first.endsWith("/")||!exe.exists())&&first.length()>=0)
    174         {
    175             int index=first.lastIndexOf('/');
    176
    177             first=first.substring(0,index);
    178             last=both.substring(index,both.length());
    179             exe=new File(_docRoot,first);
    180         }
    181
    182         if (first.length()==0||!exe.exists()||exe.isDirectory()||!exe.getCanonicalPath().equals(exe.getAbsolutePath()))
    183         {
    184             res.sendError(404);
    185         }
    186         else
    187         {
    188             if (Log.isDebugEnabled())
    189             {
    190                 Log.debug("CGI: script is "+exe);
    191                 Log.debug("CGI: pathInfo is "+last);
    192             }
    193             exec(exe,last,req,res);
    194         }
    195     }
    196
    197     /* ------------------------------------------------------------ */
    198     /*
    199      * @param root @param path @param req @param res @exception IOException
    200      */
    201     private void exec(File command, String pathInfo, HttpServletRequest req, HttpServletResponse res) throws IOException
    202     {
    203         String path=command.getAbsolutePath();
    204         File dir=command.getParentFile();
    205         String scriptName=req.getRequestURI().substring(0,req.getRequestURI().length()-pathInfo.length());
    206         String scriptPath=getServletContext().getRealPath(scriptName);
    207         String pathTranslated=req.getPathTranslated();
    208
    209         int len=req.getContentLength();
    210         if (len<0)
    211             len=0;
    212         if ((pathTranslated==null)||(pathTranslated.length()==0))
    213             pathTranslated=path;
    214
    215         EnvList env=new EnvList(_env);
    216         // these ones are from "The WWW Common Gateway Interface Version 1.1"
    217         // look at :
    218         // http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1
    219         env.set("AUTH_TYPE",req.getAuthType());
    220         env.set("CONTENT_LENGTH",Integer.toString(len));
    221         env.set("CONTENT_TYPE",req.getContentType());
    222         env.set("GATEWAY_INTERFACE","CGI/1.1");
    223         if ((pathInfo!=null)&&(pathInfo.length()>0))
    224         {
    225             env.set("PATH_INFO",pathInfo);
    226         }
    227         env.set("PATH_TRANSLATED",pathTranslated);
    228         env.set("QUERY_STRING",req.getQueryString());
    229         env.set("REMOTE_ADDR",req.getRemoteAddr());
    230         env.set("REMOTE_HOST",req.getRemoteHost());
    231         // The identity information reported about the connection by a
    232         // RFC 1413 [11] request to the remote agent, if
    233         // available. Servers MAY choose not to support this feature, or
    234         // not to request the data for efficiency reasons.
    235         // "REMOTE_IDENT" => "NYI"
    236         env.set("REMOTE_USER",req.getRemoteUser());
    237         env.set("REQUEST_METHOD",req.getMethod());
    238         env.set("SCRIPT_NAME",scriptName);
    239         env.set("SCRIPT_FILENAME",scriptPath);
    240         env.set("SERVER_NAME",req.getServerName());
    241         env.set("SERVER_PORT",Integer.toString(req.getServerPort()));
    242         env.set("SERVER_PROTOCOL",req.getProtocol());
    243         env.set("SERVER_SOFTWARE",getServletContext().getServerInfo());
    244
    245         Enumeration enm=req.getHeaderNames();
    246         while (enm.hasMoreElements())
    247         {
    248             String name=(String)enm.nextElement();
    249             String value=req.getHeader(name);
    250             env.set("HTTP_"+name.toUpperCase().replace('-','_'),value);
    251         }
    252
    253         // these extra ones were from printenv on www.dev.nomura.co.uk
    254         env.set("HTTPS",(req.isSecure()?"ON":"OFF"));
    255         // "DOCUMENT_ROOT" => root + "/docs",
    256         // "SERVER_URL" => "NYI - http://us0245",
    257         // "TZ" => System.getProperty("user.timezone"),
    258
    259         // are we meant to decode args here ? or does the script get them
    260         // via PATH_INFO ? if we are, they should be decoded and passed
    261         // into exec here...
    262         String execCmd=path;
    263         if ((execCmd.charAt(0)!='"')&&(execCmd.indexOf(" ")>=0))
    264             execCmd="\""+execCmd+"\"";
    265         if (_cmdPrefix!=null)
    266             execCmd=_cmdPrefix+" "+execCmd;
    267
    268         Process p=(dir==null)?Runtime.getRuntime().exec(execCmd,env.getEnvArray()):Runtime.getRuntime().exec(execCmd,env.getEnvArray(),dir);
    269
    270         // hook processes input to browser's output (async)
    271         final InputStream inFromReq=req.getInputStream();
    272         final OutputStream outToCgi=p.getOutputStream();
    273         final int inLength=len;
    274
    275         IO.copyThread(p.getErrorStream(),System.err);
    276
    277         new Thread(new Runnable()
    278         {
    279             public void run()
    280             {
    281                 try
    282                 {
    283                     if (inLength>0)
    284                         IO.copy(inFromReq,outToCgi,inLength);
    285                     outToCgi.close();
    286                 }
    287                 catch (IOException e)
    288                 {
    289                     Log.ignore(e);
    290                 }
    291             }
    292         }).start();
    293
    294         // hook processes output to browser's input (sync)
    295         // if browser closes stream, we should detect it and kill process...
    296         OutputStream os = null;
    297         try
    298         {
    299             // read any headers off the top of our input stream
    300             // NOTE: Multiline header items not supported!
    301             String line=null;
    302             InputStream inFromCgi=p.getInputStream();
    303
    304             //br=new BufferedReader(new InputStreamReader(inFromCgi));
    305             //while ((line=br.readLine())!=null)
    306             while( (line = getTextLineFromStream( inFromCgi )).length() > 0 )
    307             {
    308                 if (!line.startsWith("HTTP"))
    309                 {
    310                     int k=line.indexOf(':');
    311                     if (k>0)
    312                     {
    313                         String key=line.substring(0,k).trim();
    314                         String value = line.substring(k+1).trim();
    315                         if ("Location".equals(key))
    316                         {
    317                             res.sendRedirect(value);
    318                         }
    319                         else if ("Status".equals(key))
    320                         {
    321                         	String[] token = value.split( " " );
    322                             int status=Integer.parseInt(token[0]);
    323                             res.setStatus(status);
    324                         }
    325                         else
    326                         {
    327                             // add remaining header items to our response header
    328                             res.addHeader(key,value);
    329                         }
    330                     }
    331                 }
    332             }
    333             // copy cgi content to response stream...
    334             os = res.getOutputStream();
    335             IO.copy(inFromCgi, os);
    336             p.waitFor();
    337
    338             if (!_ignoreExitState)
    339             {
    340                 int exitValue=p.exitValue();
    341                 if (0!=exitValue)
    342                 {
    343                     Log.warn("Non-zero exit status ("+exitValue+") from CGI program: "+path);
    344                     if (!res.isCommitted())
    345                         res.sendError(500,"Failed to exec CGI");
    346                 }
    347             }
    348         }
    349         catch (IOException e)
    350         {
    351             // browser has probably closed its input stream - we
    352             // terminate and clean up...
    353             Log.debug("CGI: Client closed connection!");
    354         }
    355         catch (InterruptedException ie)
    356         {
    357             Log.debug("CGI: interrupted!");
    358         }
    359         finally
    360         {
    361             if( os != null )
    362             	os.close();
    363             os = null;
    364             p.destroy();
    365             // Log.debug("CGI: terminated!");
    366         }
    367     }
    368
    369     /**
    370      * Utility method to get a line of text from the input stream.
    371      * @param is the input stream
    372      * @return the line of text
    373      * @throws IOException
    374      */
    375     private String getTextLineFromStream( InputStream is ) throws IOException {
    376         StringBuffer buffer = new StringBuffer();
    377         int b;
    378
    379        	while( (b = is.read()) != -1 && b != (int) '\n' ) {
    380        		buffer.append( (char) b );
    381        	}
    382        	return buffer.toString().trim();
    383     }
    384     /* ------------------------------------------------------------ */
    385     /**
    386      * private utility class that manages the Environment passed to exec.
    387      */
    388     private static class EnvList
    389     {
    390         private Map envMap;
    391
    392         EnvList()
    393         {
    394             envMap=new HashMap();
    395         }
    396
    397         EnvList(EnvList l)
    398         {
    399             envMap=new HashMap(l.envMap);
    400         }
    401
    402         /**
    403          * Set a name/value pair, null values will be treated as an empty String
    404          */
    405         public void set(String name, String value)
    406         {
    407             envMap.put(name,name+"="+StringUtil.nonNull(value));
    408         }
    409
    410         /** Get representation suitable for passing to exec. */
    411         public String[] getEnvArray()
    412         {
    413             return (String[])envMap.values().toArray(new String[envMap.size()]);
    414         }
    415
    416         public String toString()
    417         {
    418             return envMap.toString();
    419         }
    420     }
    421 }