001: /*
002: * Copyright 1999,2004 The Apache Software Foundation.
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: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.apache.catalina.util;
018:
019: import java.io.BufferedOutputStream;
020: import java.io.BufferedReader;
021: import java.io.BufferedWriter;
022: import java.io.File;
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.io.InputStreamReader;
026: import java.io.OutputStreamWriter;
027: import java.util.Enumeration;
028: import java.util.Hashtable;
029: import java.util.Vector;
030:
031: import javax.servlet.http.HttpServletResponse;
032:
033: /**
034: * Encapsulates the knowledge of how to run a CGI script, given the
035: * script's desired environment and (optionally) input/output streams
036: *
037: * <p>
038: *
039: * Exposes a <code>run</code> method used to actually invoke the
040: * CGI.
041: *
042: * </p>
043: * <p>
044: *
045: * The CGI environment and settings are derived from the information
046: * passed to the constuctor.
047: *
048: * </p>
049: * <p>
050: *
051: * The input and output streams can be set by the <code>setInput</code>
052: * and <code>setResponse</code> methods, respectively.
053: * </p>
054: *
055: * @author Martin Dengler [root@martindengler.com]
056: * @version $Revision: 1.5 $, $Date: 2004/05/26 16:26:05 $
057: */
058: public class ProcessHelper {
059:
060: /** script/command to be executed */
061: private String command = null;
062:
063: /** environment used when invoking the cgi script */
064: private Hashtable env = null;
065:
066: /** working directory used when invoking the cgi script */
067: private File wd = null;
068:
069: /** query parameters to be passed to the invoked script */
070: private Hashtable params = null;
071:
072: /** stdin to be passed to cgi script */
073: private InputStream stdin = null;
074:
075: /** response object used to set headers & get output stream */
076: private HttpServletResponse response = null;
077:
078: /** boolean tracking whether this object has enough info to run() */
079: private boolean readyToRun = false;
080:
081: /** the debugging detail level for this instance. */
082: private int debug = 0;
083:
084: /** the time in ms to wait for the client to send us CGI input data */
085: private int iClientInputTimeout;
086:
087: /**
088: * Creates a ProcessHelper and initializes its environment, working
089: * directory, and query parameters.
090: * <BR>
091: * Input/output streams (optional) are set using the
092: * <code>setInput</code> and <code>setResponse</code> methods,
093: * respectively.
094: *
095: * @param command string full path to command to be executed
096: * @param env Hashtable with the desired script environment
097: * @param wd File with the script's desired working directory
098: * @param params Hashtable with the script's query parameters
099: */
100: public ProcessHelper(String command, Hashtable env, File wd,
101: Hashtable params) {
102: this .command = command;
103: this .env = env;
104: this .wd = wd;
105: this .params = params;
106: updateReadyStatus();
107: }
108:
109: /**
110: * Checks & sets ready status
111: */
112: protected void updateReadyStatus() {
113: if (command != null && env != null && wd != null
114: && params != null && response != null) {
115: readyToRun = true;
116: } else {
117: readyToRun = false;
118: }
119: }
120:
121: /**
122: * Gets ready status
123: *
124: * @return false if not ready (<code>run</code> will throw
125: * an exception), true if ready
126: */
127: public boolean isReady() {
128: return readyToRun;
129: }
130:
131: /**
132: * Sets HttpServletResponse object used to set headers and send
133: * output to
134: *
135: * @param response HttpServletResponse to be used
136: *
137: */
138: public void setResponse(HttpServletResponse response) {
139: this .response = response;
140: updateReadyStatus();
141: }
142:
143: /**
144: * Sets standard input to be passed on to the invoked cgi script
145: *
146: * @param stdin InputStream to be used
147: *
148: */
149: public void setInput(InputStream stdin) {
150: this .stdin = stdin;
151: updateReadyStatus();
152: }
153:
154: /**
155: * Converts a Hashtable to a String array by converting each
156: * key/value pair in the Hashtable to a String in the form
157: * "key=value" (hashkey + "=" + hash.get(hashkey).toString())
158: *
159: * @param h Hashtable to convert
160: *
161: * @return converted string array
162: *
163: * @exception NullPointerException if a hash key has a null value
164: *
165: */
166: private String[] hashToStringArray(Hashtable h)
167: throws NullPointerException {
168: Vector v = new Vector();
169: Enumeration e = h.keys();
170: while (e.hasMoreElements()) {
171: String k = e.nextElement().toString();
172: v.add(k + "=" + h.get(k));
173: }
174: String[] strArr = new String[v.size()];
175: v.copyInto(strArr);
176: return strArr;
177: }
178:
179: /**
180: * Executes a process script with the desired environment, current working
181: * directory, and input/output streams
182: *
183: * <p>
184: * This implements the following CGI specification recommedations:
185: * <UL>
186: * <LI> Servers SHOULD provide the "<code>query</code>" component of
187: * the script-URI as command-line arguments to scripts if it
188: * does not contain any unencoded "=" characters and the
189: * command-line arguments can be generated in an unambiguous
190: * manner.
191: * <LI> Servers SHOULD set the AUTH_TYPE metavariable to the value
192: * of the "<code>auth-scheme</code>" token of the
193: * "<code>Authorization</code>" if it was supplied as part of the
194: * request header. See <code>getCGIEnvironment</code> method.
195: * <LI> Where applicable, servers SHOULD set the current working
196: * directory to the directory in which the script is located
197: * before invoking it.
198: * <LI> Server implementations SHOULD define their behavior for the
199: * following cases:
200: * <ul>
201: * <LI> <u>Allowed characters in pathInfo</u>: This implementation
202: * does not allow ASCII NUL nor any character which cannot
203: * be URL-encoded according to internet standards;
204: * <LI> <u>Allowed characters in path segments</u>: This
205: * implementation does not allow non-terminal NULL
206: * segments in the the path -- IOExceptions may be thrown;
207: * <LI> <u>"<code>.</code>" and "<code>..</code>" path
208: * segments</u>:
209: * This implementation does not allow "<code>.</code>" and
210: * "<code>..</code>" in the the path, and such characters
211: * will result in an IOException being thrown;
212: * <LI> <u>Implementation limitations</u>: This implementation
213: * does not impose any limitations except as documented
214: * above. This implementation may be limited by the
215: * servlet container used to house this implementation.
216: * In particular, all the primary CGI variable values
217: * are derived either directly or indirectly from the
218: * container's implementation of the Servlet API methods.
219: * </ul>
220: * </UL>
221: * </p>
222: *
223: * For more information, see java.lang.Runtime#exec(String command,
224: * String[] envp, File dir)
225: *
226: * @exception IOException if problems during reading/writing occur
227: *
228: */
229: public void run() throws IOException {
230:
231: /*
232: * REMIND: this method feels too big; should it be re-written?
233: */
234:
235: if (!isReady()) {
236: throw new IOException(this .getClass().getName()
237: + ": not ready to run.");
238: }
239:
240: if (debug >= 1) {
241: log("runCGI(envp=[" + env + "], command=" + command + ")");
242: }
243:
244: if ((command.indexOf(File.separator + "." + File.separator) >= 0)
245: || (command.indexOf(File.separator + "..") >= 0)
246: || (command.indexOf(".." + File.separator) >= 0)) {
247: throw new IOException(this .getClass().getName()
248: + "Illegal Character in CGI command "
249: + "path ('.' or '..') detected. Not "
250: + "running CGI [" + command + "].");
251: }
252:
253: /* original content/structure of this section taken from
254: * http://developer.java.sun.com/developer/
255: * bugParade/bugs/4216884.html
256: * with major modifications by Martin Dengler
257: */
258: Runtime rt = null;
259: BufferedReader commandsStdOut = null;
260: BufferedReader commandsStdErr = null;
261: BufferedOutputStream commandsStdIn = null;
262: Process proc = null;
263: byte[] bBuf = new byte[1024];
264: char[] cBuf = new char[1024];
265: int bufRead = -1;
266:
267: //create query arguments
268: Enumeration paramNames = params.keys();
269: StringBuffer cmdAndArgs = new StringBuffer(command);
270: if (paramNames != null && paramNames.hasMoreElements()) {
271: cmdAndArgs.append(" ");
272: while (paramNames.hasMoreElements()) {
273: String k = (String) paramNames.nextElement();
274: String v = params.get(k).toString();
275: if ((k.indexOf("=") < 0) && (v.indexOf("=") < 0)) {
276: cmdAndArgs.append(k);
277: cmdAndArgs.append("=");
278: v = java.net.URLEncoder.encode(v);
279: cmdAndArgs.append(v);
280: cmdAndArgs.append(" ");
281: }
282: }
283: }
284:
285: String postIn = getPostInput(params);
286: int contentLength = (postIn.length() + System.getProperty(
287: "line.separator").length());
288: if ("POST".equals(env.get("REQUEST_METHOD"))) {
289: env.put("CONTENT_LENGTH", new Integer(contentLength));
290: }
291:
292: rt = Runtime.getRuntime();
293: proc = rt.exec(cmdAndArgs.toString(), hashToStringArray(env),
294: wd);
295:
296: /*
297: * provide input to cgi
298: * First -- parameters
299: * Second -- any remaining input
300: */
301: commandsStdIn = new BufferedOutputStream(proc.getOutputStream());
302: if (debug >= 2) {
303: log("runCGI stdin=[" + stdin + "], qs="
304: + env.get("QUERY_STRING"));
305: }
306: if ("POST".equals(env.get("REQUEST_METHOD"))) {
307: if (debug >= 2) {
308: log("runCGI: writing ---------------\n");
309: log(postIn);
310: log("runCGI: new content_length=" + contentLength
311: + "---------------\n");
312: }
313: commandsStdIn.write(postIn.getBytes());
314: }
315: if (stdin != null) {
316: //REMIND: document this
317: /* assume if nothing is available after a time, that nothing is
318: * coming...
319: */
320: if (stdin.available() <= 0) {
321: if (debug >= 2) {
322: log("runCGI stdin is NOT available ["
323: + stdin.available() + "]");
324: }
325: try {
326: Thread.sleep(iClientInputTimeout);
327: } catch (InterruptedException ignored) {
328: }
329: }
330: if (stdin.available() > 0) {
331: if (debug >= 2) {
332: log("runCGI stdin IS available ["
333: + stdin.available() + "]");
334: }
335: bBuf = new byte[1024];
336: bufRead = -1;
337: try {
338: while ((bufRead = stdin.read(bBuf)) != -1) {
339: if (debug >= 2) {
340: log("runCGI: read [" + bufRead
341: + "] bytes from stdin");
342: }
343: commandsStdIn.write(bBuf, 0, bufRead);
344: }
345: if (debug >= 2) {
346: log("runCGI: DONE READING from stdin");
347: }
348: } catch (IOException ioe) {
349: //REMIND: replace with logging
350: //REMIND: should I throw this exception?
351: log("runCGI: couldn't write all bytes.");
352: ioe.printStackTrace();
353: }
354: }
355: }
356: commandsStdIn.flush();
357: commandsStdIn.close();
358:
359: /* we want to wait for the process to exit, Process.waitFor()
360: * is useless in our situation; see
361: * http://developer.java.sun.com/developer/
362: * bugParade/bugs/4223650.html
363: */
364:
365: boolean isRunning = true;
366: commandsStdOut = new BufferedReader(new InputStreamReader(proc
367: .getInputStream()));
368: commandsStdErr = new BufferedReader(new InputStreamReader(proc
369: .getErrorStream()));
370: BufferedWriter servletContainerStdout = null;
371:
372: try {
373: if (response.getOutputStream() != null) {
374: servletContainerStdout = new BufferedWriter(
375: new OutputStreamWriter(response
376: .getOutputStream()));
377: }
378: } catch (IOException ignored) {
379: //NOOP: no output will be written
380: }
381:
382: while (isRunning) {
383:
384: try {
385: //read stderr first
386: cBuf = new char[1024];
387: while ((bufRead = commandsStdErr.read(cBuf)) != -1) {
388: if (servletContainerStdout != null) {
389: servletContainerStdout.write(cBuf, 0, bufRead);
390: }
391: }
392:
393: //set headers
394: String line = null;
395: while (((line = commandsStdOut.readLine()) != null)
396: && !("".equals(line))) {
397: if (debug >= 2) {
398: log("runCGI: addHeader(\"" + line + "\")");
399: }
400: if (line.startsWith("HTTP")) {
401: //TODO: should set status codes (NPH support)
402: /*
403: * response.setStatus(getStatusCode(line));
404: */
405: } else {
406: response.addHeader(line.substring(0,
407: line.indexOf(":")).trim(), line
408: .substring(line.indexOf(":") + 1)
409: .trim());
410: }
411: }
412:
413: //write output
414: cBuf = new char[1024];
415: while ((bufRead = commandsStdOut.read(cBuf)) != -1) {
416: if (servletContainerStdout != null) {
417: if (debug >= 4) {
418: log("runCGI: write(\"" + new String(cBuf)
419: + "\")");
420: }
421: servletContainerStdout.write(cBuf, 0, bufRead);
422: }
423: }
424:
425: if (servletContainerStdout != null) {
426: servletContainerStdout.flush();
427: }
428:
429: proc.exitValue(); // Throws exception if alive
430:
431: isRunning = false;
432:
433: } catch (IllegalThreadStateException e) {
434: try {
435: Thread.sleep(500);
436: } catch (InterruptedException ignored) {
437: }
438: }
439: } //replacement for Process.waitFor()
440:
441: }
442:
443: /**
444: * Gets a string for input to a POST cgi script
445: *
446: * @param params Hashtable of query parameters to be passed to
447: * the CGI script
448: * @return for use as input to the CGI script
449: */
450:
451: protected String getPostInput(Hashtable params) {
452: String lineSeparator = System.getProperty("line.separator");
453: Enumeration paramNames = params.keys();
454: StringBuffer postInput = new StringBuffer("");
455: StringBuffer qs = new StringBuffer("");
456: if (paramNames != null && paramNames.hasMoreElements()) {
457: while (paramNames.hasMoreElements()) {
458: String k = (String) paramNames.nextElement();
459: String v = params.get(k).toString();
460: if ((k.indexOf("=") < 0) && (v.indexOf("=") < 0)) {
461: postInput.append(k);
462: qs.append(k);
463: postInput.append("=");
464: qs.append("=");
465: postInput.append(v);
466: qs.append(v);
467: postInput.append(lineSeparator);
468: qs.append("&");
469: }
470: }
471: }
472: qs.append(lineSeparator);
473: return qs.append(postInput).toString();
474: }
475:
476: private void log(String s) {
477: System.out.println(s);
478: }
479:
480: public int getIClientInputTimeout() {
481: return iClientInputTimeout;
482: }
483:
484: public void setIClientInputTimeout(int iClientInputTimeout) {
485: this.iClientInputTimeout = iClientInputTimeout;
486: }
487: }
|