001: //========================================================================
002: //Copyright 2006 Mort Bay Consulting Pty. Ltd.
003: //------------------------------------------------------------------------
004: //Licensed under the Apache License, Version 2.0 (the "License");
005: //you may not use this file except in compliance with the License.
006: //You may obtain a copy of the License at
007: //http://www.apache.org/licenses/LICENSE-2.0
008: //Unless required by applicable law or agreed to in writing, software
009: //distributed under the License is distributed on an "AS IS" BASIS,
010: //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011: //See the License for the specific language governing permissions and
012: //limitations under the License.
013: //========================================================================
014:
015: package org.mortbay.servlet;
016:
017: import java.io.BufferedReader;
018: import java.io.File;
019: import java.io.IOException;
020: import java.io.InputStream;
021: import java.io.InputStreamReader;
022: import java.io.OutputStream;
023: import java.io.PrintWriter;
024: import java.util.Enumeration;
025: import java.util.HashMap;
026: import java.util.Map;
027:
028: import javax.servlet.ServletException;
029: import javax.servlet.http.HttpServlet;
030: import javax.servlet.http.HttpServletRequest;
031: import javax.servlet.http.HttpServletResponse;
032:
033: import org.mortbay.log.Log;
034: import org.mortbay.util.IO;
035: import org.mortbay.util.StringUtil;
036:
037: //-----------------------------------------------------------------------------
038: /** CGI Servlet.
039: *
040: * The cgi bin directory can be set with the cgibinResourceBase init
041: * parameter or it will default to the resource base of the context.
042: *
043: * The "commandPrefix" init parameter may be used to set a prefix to all
044: * commands passed to exec. This can be used on systems that need assistance
045: * to execute a particular file type. For example on windows this can be set
046: * to "perl" so that perl scripts are executed.
047: *
048: * The "Path" init param is passed to the exec environment as PATH.
049: * Note: Must be run unpacked somewhere in the filesystem.
050: *
051: * Any initParameter that starts with ENV_ is used to set an environment
052: * variable with the name stripped of the leading ENV_ and using the init
053: * parameter value.
054: *
055: * @author Julian Gosnell
056: * @author Thanassis Papathanasiou - Some minor modifications for Jetty6 port
057: */
058: public class CGI extends HttpServlet {
059: private File _docRoot;
060: private String _path;
061: private String _cmdPrefix;
062: private EnvList _env;
063: private boolean _ignoreExitState;
064:
065: /* ------------------------------------------------------------ */
066: public void init() throws ServletException {
067: _env = new EnvList();
068: _cmdPrefix = getInitParameter("commandPrefix");
069:
070: String tmp = getInitParameter("cgibinResourceBase");
071: if (tmp == null) {
072: tmp = getInitParameter("resourceBase");
073: if (tmp == null)
074: tmp = getServletContext().getRealPath("/");
075: }
076:
077: if (tmp == null) {
078: Log.warn("CGI: no CGI bin !");
079: throw new ServletException();
080: }
081:
082: File dir = new File(tmp);
083: if (!dir.exists()) {
084: Log.warn("CGI: CGI bin does not exist - " + dir);
085: throw new ServletException();
086: }
087:
088: if (!dir.canRead()) {
089: Log.warn("CGI: CGI bin is not readable - " + dir);
090: throw new ServletException();
091: }
092:
093: if (!dir.isDirectory()) {
094: Log.warn("CGI: CGI bin is not a directory - " + dir);
095: throw new ServletException();
096: }
097:
098: try {
099: _docRoot = dir.getCanonicalFile();
100: } catch (IOException e) {
101: Log.warn("CGI: CGI bin failed - " + dir);
102: e.printStackTrace();
103: throw new ServletException();
104: }
105:
106: _path = getInitParameter("Path");
107: if (_path != null)
108: _env.set("PATH", _path);
109:
110: _ignoreExitState = "true"
111: .equalsIgnoreCase(getInitParameter("ignoreExitState"));
112: Enumeration e = getInitParameterNames();
113: while (e.hasMoreElements()) {
114: String n = (String) e.nextElement();
115: if (n != null && n.startsWith("ENV_"))
116: _env.set(n.substring(4), getInitParameter(n));
117: }
118: }
119:
120: /* ------------------------------------------------------------ */
121: public void service(HttpServletRequest req, HttpServletResponse res)
122: throws ServletException, IOException {
123: String pathInContext = StringUtil.nonNull(req.getServletPath())
124: + StringUtil.nonNull(req.getPathInfo());
125:
126: if (Log.isDebugEnabled()) {
127: Log.debug("CGI: ContextPath : " + req.getContextPath());
128: Log.debug("CGI: ServletPath : " + req.getServletPath());
129: Log.debug("CGI: PathInfo : " + req.getPathInfo());
130: Log.debug("CGI: _docRoot : " + _docRoot);
131: Log.debug("CGI: _path : " + _path);
132: Log.debug("CGI: _ignoreExitState: " + _ignoreExitState);
133: }
134:
135: // pathInContext may actually comprises scriptName/pathInfo...We will
136: // walk backwards up it until we find the script - the rest must
137: // be the pathInfo;
138:
139: String both = pathInContext;
140: String first = both;
141: String last = "";
142:
143: File exe = new File(_docRoot, first);
144:
145: while ((first.endsWith("/") || !exe.exists())
146: && first.length() >= 0) {
147: int index = first.lastIndexOf('/');
148:
149: first = first.substring(0, index);
150: last = both.substring(index, both.length());
151: exe = new File(_docRoot, first);
152: }
153:
154: if (first.length() == 0
155: || !exe.exists()
156: || exe.isDirectory()
157: || !exe.getCanonicalPath()
158: .equals(exe.getAbsolutePath())) {
159: res.sendError(404);
160: } else {
161: if (Log.isDebugEnabled()) {
162: Log.debug("CGI: script is " + exe);
163: Log.debug("CGI: pathInfo is " + last);
164: }
165: exec(exe, last, req, res);
166: }
167: }
168:
169: /* ------------------------------------------------------------ */
170: /*
171: * @param root
172: * @param path
173: * @param req
174: * @param res
175: * @exception IOException
176: */
177: private void exec(File command, String pathInfo,
178: HttpServletRequest req, HttpServletResponse res)
179: throws IOException {
180: String path = command.getAbsolutePath();
181: File dir = command.getParentFile();
182: String scriptName = req.getRequestURI().substring(0,
183: req.getRequestURI().length() - pathInfo.length());
184: String scriptPath = getServletContext().getRealPath(scriptName);
185: String pathTranslated = req.getPathTranslated();
186:
187: int len = req.getContentLength();
188: if (len < 0)
189: len = 0;
190: if ((pathTranslated == null) || (pathTranslated.length() == 0))
191: pathTranslated = path;
192:
193: EnvList env = new EnvList(_env);
194: // these ones are from "The WWW Common Gateway Interface Version 1.1"
195: // look at : http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1
196: env.set("AUTH_TYPE", req.getAuthType());
197: env.set("CONTENT_LENGTH", Integer.toString(len));
198: env.set("CONTENT_TYPE", req.getContentType());
199: env.set("GATEWAY_INTERFACE", "CGI/1.1");
200: if ((pathInfo != null) && (pathInfo.length() > 0)) {
201: env.set("PATH_INFO", pathInfo);
202: }
203: env.set("PATH_TRANSLATED", pathTranslated);
204: env.set("QUERY_STRING", req.getQueryString());
205: env.set("REMOTE_ADDR", req.getRemoteAddr());
206: env.set("REMOTE_HOST", req.getRemoteHost());
207: // The identity information reported about the connection by a
208: // RFC 1413 [11] request to the remote agent, if
209: // available. Servers MAY choose not to support this feature, or
210: // not to request the data for efficiency reasons.
211: // "REMOTE_IDENT" => "NYI"
212: env.set("REMOTE_USER", req.getRemoteUser());
213: env.set("REQUEST_METHOD", req.getMethod());
214: env.set("SCRIPT_NAME", scriptName);
215: env.set("SCRIPT_FILENAME", scriptPath);
216: env.set("SERVER_NAME", req.getServerName());
217: env.set("SERVER_PORT", Integer.toString(req.getServerPort()));
218: env.set("SERVER_PROTOCOL", req.getProtocol());
219: env.set("SERVER_SOFTWARE", getServletContext().getServerInfo());
220:
221: Enumeration enm = req.getHeaderNames();
222: while (enm.hasMoreElements()) {
223: String name = (String) enm.nextElement();
224: String value = req.getHeader(name);
225: env.set("HTTP_" + name.toUpperCase().replace('-', '_'),
226: value);
227: }
228:
229: // these extra ones were from printenv on www.dev.nomura.co.uk
230: env.set("HTTPS", (req.isSecure() ? "ON" : "OFF"));
231: // "DOCUMENT_ROOT" => root + "/docs",
232: // "SERVER_URL" => "NYI - http://us0245",
233: // "TZ" => System.getProperty("user.timezone"),
234:
235: // are we meant to decode args here ? or does the script get them
236: // via PATH_INFO ? if we are, they should be decoded and passed
237: // into exec here...
238: String execCmd = path;
239: if ((execCmd.charAt(0) != '"') && (execCmd.indexOf(" ") >= 0))
240: execCmd = "\"" + execCmd + "\"";
241: if (_cmdPrefix != null)
242: execCmd = _cmdPrefix + " " + execCmd;
243:
244: Process p = (dir == null) ? Runtime.getRuntime().exec(execCmd,
245: env.getEnvArray()) : Runtime.getRuntime().exec(execCmd,
246: env.getEnvArray(), dir);
247:
248: // hook processes input to browser's output (async)
249: final InputStream inFromReq = req.getInputStream();
250: final OutputStream outToCgi = p.getOutputStream();
251: final int inLength = len;
252:
253: new Thread(new Runnable() {
254: public void run() {
255: try {
256: if (inLength > 0)
257: IO.copy(inFromReq, outToCgi, inLength);
258: outToCgi.close();
259: } catch (IOException e) {
260: Log.ignore(e);
261: }
262: }
263: }).start();
264:
265: // hook processes output to browser's input (sync)
266: // if browser closes stream, we should detect it and kill process...
267: BufferedReader br = null;
268: PrintWriter writer = null;
269: try {
270: // read any headers off the top of our input stream
271: // NOTE: Multiline header items not supported!
272: String line = null;
273: InputStream inFromCgi = p.getInputStream();
274:
275: br = new BufferedReader(new InputStreamReader(inFromCgi));
276: while ((line = br.readLine()) != null) {
277: // check for end of header
278: if (line.trim().length() == 0)
279: break;
280: if (!line.startsWith("HTTP")) {
281: int k = line.indexOf(':');
282: if (k > 0) {
283: String key = line.substring(0, k).trim();
284: if ("Location".equals(key)) {
285: res.sendRedirect(line.substring(k + 1)
286: .trim());
287: } else if ("Status".equals(key)) {
288: int status = Integer.parseInt(line
289: .substring(k + 1).trim());
290: res.setStatus(status);
291: } else {
292: // add remaining header items to our response header
293: res.addHeader(key, line.substring(k + 1));
294: }
295: }
296: }
297: }
298: // copy cgi content to response stream...
299: writer = res.getWriter();
300: IO.copy(br, writer);
301: p.waitFor();
302:
303: if (!_ignoreExitState) {
304: int exitValue = p.exitValue();
305: if (0 != exitValue) {
306: Log.warn("Non-zero exit status (" + exitValue
307: + ") from CGI program: " + path);
308: if (!res.isCommitted())
309: res.sendError(500, "Failed to exec CGI");
310: }
311: }
312: } catch (IOException e) {
313: // browser has probably closed its input stream - we
314: // terminate and clean up...
315: Log.debug("CGI: Client closed connection!");
316: } catch (InterruptedException ie) {
317: Log.debug("CGI: interrupted!");
318: } finally {
319: if (br != null)
320: try {
321: br.close();
322: } catch (IOException ioe) {
323: }
324: if (writer != null)
325: writer.close();
326: br = null;
327: writer = null;
328: p.destroy();
329: // Log.debug("CGI: terminated!");
330: }
331: }
332:
333: /* ------------------------------------------------------------ */
334: /** private utility class that manages the Environment passed
335: * to exec.
336: */
337: private static class EnvList {
338: private Map envMap;
339:
340: EnvList() {
341: envMap = new HashMap();
342: }
343:
344: EnvList(EnvList l) {
345: envMap = new HashMap(l.envMap);
346: }
347:
348: /** Set a name/value pair, null values will be treated as
349: * an empty String
350: */
351: public void set(String name, String value) {
352: envMap.put(name, name + "=" + StringUtil.nonNull(value));
353: }
354:
355: /** Get representation suitable for passing to exec. */
356: public String[] getEnvArray() {
357: return (String[]) envMap.values().toArray(
358: new String[envMap.size()]);
359: }
360:
361: public String toString() {
362: return envMap.toString();
363: }
364: }
365: }
|