001: /*
002: * ProcessHelper.java $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/util/ProcessHelper.java,v 1.3 2001/09/20 23:48:13 remm Exp $
003: * $Revision: 1.3 $, $Date: 2001/09/20 23:48:13 $
004: *
005: * ====================================================================
006: *
007: * The Apache Software License, Version 1.1
008: *
009: * Copyright (c) 1999 The Apache Software Foundation. All rights
010: * reserved.
011: *
012: * Redistribution and use in source and binary forms, with or without
013: * modification, are permitted provided that the following conditions
014: * are met:
015: *
016: * 1. Redistributions of source code must retain the above copyright
017: * notice, this list of conditions and the following disclaimer.
018: *
019: * 2. Redistributions in binary form must reproduce the above copyright
020: * notice, this list of conditions and the following disclaimer in
021: * the documentation and/or other materials provided with the
022: * distribution.
023: *
024: * 3. The end-user documentation included with the redistribution, if
025: * any, must include the following acknowlegement:
026: * "This product includes software developed by the
027: * Apache Software Foundation (http://www.apache.org/)."
028: * Alternately, this acknowlegement may appear in the software itself,
029: * if and wherever such third-party acknowlegements normally appear.
030: *
031: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
032: * Foundation" must not be used to endorse or promote products derived
033: * from this software without prior written permission. For written
034: * permission, please contact apache@apache.org.
035: *
036: * 5. Products derived from this software may not be called "Apache"
037: * nor may "Apache" appear in their names without prior written
038: * permission of the Apache Group.
039: *
040: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
041: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
042: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
043: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
044: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
045: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
046: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
047: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
048: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
049: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
050: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
051: * SUCH DAMAGE.
052: * ====================================================================
053: *
054: * This software consists of voluntary contributions made by many
055: * individuals on behalf of the Apache Software Foundation. For more
056: * information on the Apache Software Foundation, please see
057: * <http://www.apache.org/>.
058: *
059: *
060: */
061:
062: package org.apache.catalina.util;
063:
064: import java.lang.Process;
065: import java.io.File;
066: import java.io.Writer;
067: import java.io.Reader;
068: import java.io.PrintWriter;
069: import java.io.BufferedWriter;
070: import java.io.BufferedReader;
071: import java.io.InputStream;
072: import java.io.OutputStream;
073: import java.io.InputStreamReader;
074: import java.io.OutputStreamWriter;
075: import java.io.BufferedInputStream;
076: import java.io.BufferedOutputStream;
077: import java.io.IOException;
078: import java.net.URLEncoder;
079: import java.util.Hashtable;
080: import java.util.Vector;
081: import java.util.Enumeration;
082: import java.util.StringTokenizer;
083: import java.util.Locale;
084: import java.util.Date;
085: import javax.servlet.ServletException;
086: import javax.servlet.ServletOutputStream;
087: import javax.servlet.ServletContext;
088: import javax.servlet.ServletConfig;
089: import javax.servlet.http.HttpServlet;
090: import javax.servlet.http.HttpServletRequest;
091: import javax.servlet.http.HttpServletResponse;
092: import javax.servlet.http.HttpSession;
093: import javax.servlet.http.Cookie;
094: import org.apache.catalina.Context;
095: import org.apache.catalina.Wrapper;
096:
097: // import org.apache.catalina.util.StringManager;
098:
099: //class CGIServlet
100:
101: /**
102: * Encapsulates the knowledge of how to run a CGI script, given the
103: * script's desired environment and (optionally) input/output streams
104: *
105: * <p>
106: *
107: * Exposes a <code>run</code> method used to actually invoke the
108: * CGI.
109: *
110: * </p>
111: * <p>
112: *
113: * The CGI environment and settings are derived from the information
114: * passed to the constuctor.
115: *
116: * </p>
117: * <p>
118: *
119: * The input and output streams can be set by the <code>setInput</code>
120: * and <code>setResponse</code> methods, respectively.
121: * </p>
122: *
123: * @author Martin Dengler [root@martindengler.com]
124: * @version $Revision: 1.3 $, $Date: 2001/09/20 23:48:13 $
125: */
126: public class ProcessHelper {
127:
128: /** script/command to be executed */
129: private String command = null;
130:
131: /** environment used when invoking the cgi script */
132: private Hashtable env = null;
133:
134: /** working directory used when invoking the cgi script */
135: private File wd = null;
136:
137: /** query parameters to be passed to the invoked script */
138: private Hashtable params = null;
139:
140: /** stdin to be passed to cgi script */
141: private InputStream stdin = null;
142:
143: /** response object used to set headers & get output stream */
144: private HttpServletResponse response = null;
145:
146: /** boolean tracking whether this object has enough info to run() */
147: private boolean readyToRun = false;
148:
149: /** the debugging detail level for this instance. */
150: private int debug = 0;
151:
152: /** the time in ms to wait for the client to send us CGI input data */
153: private int iClientInputTimeout;
154:
155: /**
156: * Creates a ProcessHelper and initializes its environment, working
157: * directory, and query parameters.
158: * <BR>
159: * Input/output streams (optional) are set using the
160: * <code>setInput</code> and <code>setResponse</code> methods,
161: * respectively.
162: *
163: * @param command string full path to command to be executed
164: * @param env Hashtable with the desired script environment
165: * @param wd File with the script's desired working directory
166: * @param params Hashtable with the script's query parameters
167: *
168: * @param res HttpServletResponse object for setting headers
169: * based on CGI script output
170: *
171: */
172: public ProcessHelper(String command, Hashtable env, File wd,
173: Hashtable params) {
174: this .command = command;
175: this .env = env;
176: this .wd = wd;
177: this .params = params;
178: updateReadyStatus();
179: }
180:
181: /**
182: * Checks & sets ready status
183: */
184: protected void updateReadyStatus() {
185: if (command != null && env != null && wd != null
186: && params != null && response != null) {
187: readyToRun = true;
188: } else {
189: readyToRun = false;
190: }
191: }
192:
193: /**
194: * Gets ready status
195: *
196: * @return false if not ready (<code>run</code> will throw
197: * an exception), true if ready
198: */
199: public boolean isReady() {
200: return readyToRun;
201: }
202:
203: /**
204: * Sets HttpServletResponse object used to set headers and send
205: * output to
206: *
207: * @param response HttpServletResponse to be used
208: *
209: */
210: public void setResponse(HttpServletResponse response) {
211: this .response = response;
212: updateReadyStatus();
213: }
214:
215: /**
216: * Sets standard input to be passed on to the invoked cgi script
217: *
218: * @param stdin InputStream to be used
219: *
220: */
221: public void setInput(InputStream stdin) {
222: this .stdin = stdin;
223: updateReadyStatus();
224: }
225:
226: /**
227: * Converts a Hashtable to a String array by converting each
228: * key/value pair in the Hashtable to a String in the form
229: * "key=value" (hashkey + "=" + hash.get(hashkey).toString())
230: *
231: * @param h Hashtable to convert
232: *
233: * @return converted string array
234: *
235: * @exception NullPointerException if a hash key has a null value
236: *
237: */
238: private String[] hashToStringArray(Hashtable h)
239: throws NullPointerException {
240: Vector v = new Vector();
241: Enumeration e = h.keys();
242: while (e.hasMoreElements()) {
243: String k = e.nextElement().toString();
244: v.add(k + "=" + h.get(k));
245: }
246: String[] strArr = new String[v.size()];
247: v.copyInto(strArr);
248: return strArr;
249: }
250:
251: /**
252: * Executes a process script with the desired environment, current working
253: * directory, and input/output streams
254: *
255: * <p>
256: * This implements the following CGI specification recommedations:
257: * <UL>
258: * <LI> Servers SHOULD provide the "<code>query</code>" component of
259: * the script-URI as command-line arguments to scripts if it
260: * does not contain any unencoded "=" characters and the
261: * command-line arguments can be generated in an unambiguous
262: * manner.
263: * <LI> Servers SHOULD set the AUTH_TYPE metavariable to the value
264: * of the "<code>auth-scheme</code>" token of the
265: * "<code>Authorization</code>" if it was supplied as part of the
266: * request header. See <code>getCGIEnvironment</code> method.
267: * <LI> Where applicable, servers SHOULD set the current working
268: * directory to the directory in which the script is located
269: * before invoking it.
270: * <LI> Server implementations SHOULD define their behavior for the
271: * following cases:
272: * <ul>
273: * <LI> <u>Allowed characters in pathInfo</u>: This implementation
274: * does not allow ASCII NUL nor any character which cannot
275: * be URL-encoded according to internet standards;
276: * <LI> <u>Allowed characters in path segments</u>: This
277: * implementation does not allow non-terminal NULL
278: * segments in the the path -- IOExceptions may be thrown;
279: * <LI> <u>"<code>.</code>" and "<code>..</code>" path
280: * segments</u>:
281: * This implementation does not allow "<code>.</code>" and
282: * "<code>..</code>" in the the path, and such characters
283: * will result in an IOException being thrown;
284: * <LI> <u>Implementation limitations</u>: This implementation
285: * does not impose any limitations except as documented
286: * above. This implementation may be limited by the
287: * servlet container used to house this implementation.
288: * In particular, all the primary CGI variable values
289: * are derived either directly or indirectly from the
290: * container's implementation of the Servlet API methods.
291: * </ul>
292: * </UL>
293: * </p>
294: *
295: * For more information, see java.lang.Runtime#exec(String command,
296: * String[] envp, File dir)
297: *
298: * @exception IOException if problems during reading/writing occur
299: *
300: */
301: public void run() throws IOException {
302:
303: /*
304: * REMIND: this method feels too big; should it be re-written?
305: */
306:
307: if (!isReady()) {
308: throw new IOException(this .getClass().getName()
309: + ": not ready to run.");
310: }
311:
312: if (debug >= 1) {
313: log("runCGI(envp=[" + env + "], command=" + command + ")");
314: }
315:
316: if ((command.indexOf(File.separator + "." + File.separator) >= 0)
317: || (command.indexOf(File.separator + "..") >= 0)
318: || (command.indexOf(".." + File.separator) >= 0)) {
319: throw new IOException(this .getClass().getName()
320: + "Illegal Character in CGI command "
321: + "path ('.' or '..') detected. Not "
322: + "running CGI [" + command + "].");
323: }
324:
325: /* original content/structure of this section taken from
326: * http://developer.java.sun.com/developer/
327: * bugParade/bugs/4216884.html
328: * with major modifications by Martin Dengler
329: */
330: Runtime rt = null;
331: BufferedReader commandsStdOut = null;
332: BufferedReader commandsStdErr = null;
333: BufferedOutputStream commandsStdIn = null;
334: Process proc = null;
335: byte[] bBuf = new byte[1024];
336: char[] cBuf = new char[1024];
337: int bufRead = -1;
338:
339: //create query arguments
340: Enumeration paramNames = params.keys();
341: StringBuffer cmdAndArgs = new StringBuffer(command);
342: if (paramNames != null && paramNames.hasMoreElements()) {
343: cmdAndArgs.append(" ");
344: while (paramNames.hasMoreElements()) {
345: String k = (String) paramNames.nextElement();
346: String v = params.get(k).toString();
347: if ((k.indexOf("=") < 0) && (v.indexOf("=") < 0)) {
348: cmdAndArgs.append(k);
349: cmdAndArgs.append("=");
350: v = java.net.URLEncoder.encode(v);
351: cmdAndArgs.append(v);
352: cmdAndArgs.append(" ");
353: }
354: }
355: }
356:
357: String postIn = getPostInput(params);
358: int contentLength = (postIn.length() + System.getProperty(
359: "line.separator").length());
360: if ("POST".equals(env.get("REQUEST_METHOD"))) {
361: env.put("CONTENT_LENGTH", new Integer(contentLength));
362: }
363:
364: rt = Runtime.getRuntime();
365: proc = rt.exec(cmdAndArgs.toString(), hashToStringArray(env),
366: wd);
367:
368: /*
369: * provide input to cgi
370: * First -- parameters
371: * Second -- any remaining input
372: */
373: commandsStdIn = new BufferedOutputStream(proc.getOutputStream());
374: if (debug >= 2) {
375: log("runCGI stdin=[" + stdin + "], qs="
376: + env.get("QUERY_STRING"));
377: }
378: if ("POST".equals(env.get("REQUEST_METHOD"))) {
379: if (debug >= 2) {
380: log("runCGI: writing ---------------\n");
381: log(postIn);
382: log("runCGI: new content_length=" + contentLength
383: + "---------------\n");
384: }
385: commandsStdIn.write(postIn.getBytes());
386: }
387: if (stdin != null) {
388: //REMIND: document this
389: /* assume if nothing is available after a time, that nothing is
390: * coming...
391: */
392: if (stdin.available() <= 0) {
393: if (debug >= 2) {
394: log("runCGI stdin is NOT available ["
395: + stdin.available() + "]");
396: }
397: try {
398: Thread.currentThread().sleep(iClientInputTimeout);
399: } catch (InterruptedException ignored) {
400: }
401: }
402: if (stdin.available() > 0) {
403: if (debug >= 2) {
404: log("runCGI stdin IS available ["
405: + stdin.available() + "]");
406: }
407: bBuf = new byte[1024];
408: bufRead = -1;
409: try {
410: while ((bufRead = stdin.read(bBuf)) != -1) {
411: if (debug >= 2) {
412: log("runCGI: read [" + bufRead
413: + "] bytes from stdin");
414: }
415: commandsStdIn.write(bBuf, 0, bufRead);
416: }
417: if (debug >= 2) {
418: log("runCGI: DONE READING from stdin");
419: }
420: } catch (IOException ioe) {
421: //REMIND: replace with logging
422: //REMIND: should I throw this exception?
423: log("runCGI: couldn't write all bytes.");
424: ioe.printStackTrace();
425: }
426: }
427: }
428: commandsStdIn.flush();
429: commandsStdIn.close();
430:
431: /* we want to wait for the process to exit, Process.waitFor()
432: * is useless in our situation; see
433: * http://developer.java.sun.com/developer/
434: * bugParade/bugs/4223650.html
435: */
436:
437: boolean isRunning = true;
438: commandsStdOut = new BufferedReader(new InputStreamReader(proc
439: .getInputStream()));
440: commandsStdErr = new BufferedReader(new InputStreamReader(proc
441: .getErrorStream()));
442: BufferedWriter servletContainerStdout = null;
443:
444: try {
445: if (response.getOutputStream() != null) {
446: servletContainerStdout = new BufferedWriter(
447: new OutputStreamWriter(response
448: .getOutputStream()));
449: }
450: } catch (IOException ignored) {
451: //NOOP: no output will be written
452: }
453:
454: while (isRunning) {
455:
456: try {
457: //read stderr first
458: cBuf = new char[1024];
459: while ((bufRead = commandsStdErr.read(cBuf)) != -1) {
460: if (servletContainerStdout != null) {
461: servletContainerStdout.write(cBuf, 0, bufRead);
462: }
463: }
464:
465: //set headers
466: String line = null;
467: while (((line = commandsStdOut.readLine()) != null)
468: && !("".equals(line))) {
469: if (debug >= 2) {
470: log("runCGI: addHeader(\"" + line + "\")");
471: }
472: if (line.startsWith("HTTP")) {
473: //TODO: should set status codes (NPH support)
474: /*
475: * response.setStatus(getStatusCode(line));
476: */
477: } else {
478: response.addHeader(line.substring(0,
479: line.indexOf(":")).trim(), line
480: .substring(line.indexOf(":") + 1)
481: .trim());
482: }
483: }
484:
485: //write output
486: cBuf = new char[1024];
487: while ((bufRead = commandsStdOut.read(cBuf)) != -1) {
488: if (servletContainerStdout != null) {
489: if (debug >= 4) {
490: log("runCGI: write(\"" + cBuf + "\")");
491: }
492: servletContainerStdout.write(cBuf, 0, bufRead);
493: }
494: }
495:
496: if (servletContainerStdout != null) {
497: servletContainerStdout.flush();
498: }
499:
500: proc.exitValue(); // Throws exception if alive
501:
502: isRunning = false;
503:
504: } catch (IllegalThreadStateException e) {
505: try {
506: Thread.currentThread().sleep(500);
507: } catch (InterruptedException ignored) {
508: }
509: }
510: } //replacement for Process.waitFor()
511:
512: }
513:
514: /**
515: * Gets a string for input to a POST cgi script
516: *
517: * @param params Hashtable of query parameters to be passed to
518: * the CGI script
519: * @return for use as input to the CGI script
520: */
521:
522: protected String getPostInput(Hashtable params) {
523: String lineSeparator = System.getProperty("line.separator");
524: Enumeration paramNames = params.keys();
525: StringBuffer postInput = new StringBuffer("");
526: StringBuffer qs = new StringBuffer("");
527: if (paramNames != null && paramNames.hasMoreElements()) {
528: while (paramNames.hasMoreElements()) {
529: String k = (String) paramNames.nextElement();
530: String v = params.get(k).toString();
531: if ((k.indexOf("=") < 0) && (v.indexOf("=") < 0)) {
532: postInput.append(k);
533: qs.append(k);
534: postInput.append("=");
535: qs.append("=");
536: postInput.append(v);
537: qs.append(v);
538: postInput.append(lineSeparator);
539: qs.append("&");
540: }
541: }
542: }
543: qs.append(lineSeparator);
544: return qs.append(postInput).toString();
545: }
546:
547: private void log(String s) {
548: System.out.println(s);
549: }
550:
551: public int getIClientInputTimeout() {
552: return iClientInputTimeout;
553: }
554:
555: public void setIClientInputTimeout(int iClientInputTimeout) {
556: this.iClientInputTimeout = iClientInputTimeout;
557: }
558: }
|