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