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 }