1// 2// ======================================================================== 3// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. 4// ------------------------------------------------------------------------ 5// All rights reserved. This program and the accompanying materials 6// are made available under the terms of the Eclipse Public License v1.0 7// and Apache License v2.0 which accompanies this distribution. 8// 9// The Eclipse Public License is available at 10// http://www.eclipse.org/legal/epl-v10.html 11// 12// The Apache License v2.0 is available at 13// http://www.opensource.org/licenses/apache2.0.php 14// 15// You may elect to redistribute this code under either of these licenses. 16// ======================================================================== 17// 18 19package org.eclipse.jetty.servlets; 20 21import java.io.File; 22import java.io.IOException; 23import java.io.InputStream; 24import java.io.OutputStream; 25import java.io.OutputStreamWriter; 26import java.io.Writer; 27import java.util.Enumeration; 28import java.util.HashMap; 29import java.util.Locale; 30import java.util.Map; 31 32import javax.servlet.ServletException; 33import javax.servlet.http.HttpServlet; 34import javax.servlet.http.HttpServletRequest; 35import javax.servlet.http.HttpServletResponse; 36 37import org.eclipse.jetty.http.HttpMethods; 38import org.eclipse.jetty.util.IO; 39import org.eclipse.jetty.util.MultiMap; 40import org.eclipse.jetty.util.StringUtil; 41import org.eclipse.jetty.util.UrlEncoded; 42import org.eclipse.jetty.util.log.Log; 43import org.eclipse.jetty.util.log.Logger; 44 45//----------------------------------------------------------------------------- 46/** 47 * CGI Servlet. 48 * <p/> 49 * The cgi bin directory can be set with the "cgibinResourceBase" init parameter or it will default to the resource base of the context. If the 50 * "cgibinResourceBaseIsRelative" init parameter is set the resource base is relative to the webapp. For example "WEB-INF/cgi" would work. 51 * <br/> 52 * Not that this only works for extracted war files as "jar cf" will not reserve the execute permissions on the cgi files. 53 * <p/> 54 * The "commandPrefix" init parameter may be used to set a prefix to all commands passed to exec. This can be used on systems that need assistance to execute a 55 * particular file type. For example on windows this can be set to "perl" so that perl scripts are executed. 56 * <p/> 57 * The "Path" init param is passed to the exec environment as PATH. Note: Must be run unpacked somewhere in the filesystem. 58 * <p/> 59 * Any initParameter that starts with ENV_ is used to set an environment variable with the name stripped of the leading ENV_ and using the init parameter value. 60 */ 61public class CGI extends HttpServlet 62{ 63 /** 64 * 65 */ 66 private static final long serialVersionUID = -6182088932884791073L; 67 68 private static final Logger LOG = Log.getLogger(CGI.class); 69 70 private boolean _ok; 71 private File _docRoot; 72 private String _path; 73 private String _cmdPrefix; 74 private EnvList _env; 75 private boolean _ignoreExitState; 76 private boolean _relative; 77 78 /* ------------------------------------------------------------ */ 79 @Override 80 public void init() throws ServletException 81 { 82 _env = new EnvList(); 83 _cmdPrefix = getInitParameter("commandPrefix"); 84 _relative = Boolean.parseBoolean(getInitParameter("cgibinResourceBaseIsRelative")); 85 86 String tmp = getInitParameter("cgibinResourceBase"); 87 if (tmp == null) 88 { 89 tmp = getInitParameter("resourceBase"); 90 if (tmp == null) 91 tmp = getServletContext().getRealPath("/"); 92 } 93 else if (_relative) 94 { 95 tmp = getServletContext().getRealPath(tmp); 96 } 97 98 if (tmp == null) 99 { 100 LOG.warn("CGI: no CGI bin !"); 101 return; 102 } 103 104 File dir = new File(tmp); 105 if (!dir.exists()) 106 { 107 LOG.warn("CGI: CGI bin does not exist - " + dir); 108 return; 109 } 110 111 if (!dir.canRead()) 112 { 113 LOG.warn("CGI: CGI bin is not readable - " + dir); 114 return; 115 } 116 117 if (!dir.isDirectory()) 118 { 119 LOG.warn("CGI: CGI bin is not a directory - " + dir); 120 return; 121 } 122 123 try 124 { 125 _docRoot = dir.getCanonicalFile(); 126 } 127 catch (IOException e) 128 { 129 LOG.warn("CGI: CGI bin failed - " + dir,e); 130 return; 131 } 132 133 _path = getInitParameter("Path"); 134 if (_path != null) 135 _env.set("PATH",_path); 136 137 _ignoreExitState = "true".equalsIgnoreCase(getInitParameter("ignoreExitState")); 138 Enumeration e = getInitParameterNames(); 139 while (e.hasMoreElements()) 140 { 141 String n = (String)e.nextElement(); 142 if (n != null && n.startsWith("ENV_")) 143 _env.set(n.substring(4),getInitParameter(n)); 144 } 145 if (!_env.envMap.containsKey("SystemRoot")) 146 { 147 String os = System.getProperty("os.name"); 148 if (os != null && os.toLowerCase(Locale.ENGLISH).indexOf("windows") != -1) 149 { 150 _env.set("SystemRoot","C:\\WINDOWS"); 151 } 152 } 153 154 _ok = true; 155 } 156 157 /* ------------------------------------------------------------ */ 158 @Override 159 public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException 160 { 161 if (!_ok) 162 { 163 res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); 164 return; 165 } 166 167 String pathInContext = (_relative?"":StringUtil.nonNull(req.getServletPath())) + StringUtil.nonNull(req.getPathInfo()); 168 if (LOG.isDebugEnabled()) 169 { 170 LOG.debug("CGI: ContextPath : " + req.getContextPath()); 171 LOG.debug("CGI: ServletPath : " + req.getServletPath()); 172 LOG.debug("CGI: PathInfo : " + req.getPathInfo()); 173 LOG.debug("CGI: _docRoot : " + _docRoot); 174 LOG.debug("CGI: _path : " + _path); 175 LOG.debug("CGI: _ignoreExitState: " + _ignoreExitState); 176 } 177 178 // pathInContext may actually comprises scriptName/pathInfo...We will 179 // walk backwards up it until we find the script - the rest must 180 // be the pathInfo; 181 182 String both = pathInContext; 183 String first = both; 184 String last = ""; 185 186 File exe = new File(_docRoot,first); 187 188 while ((first.endsWith("/") || !exe.exists()) && first.length() >= 0) 189 { 190 int index = first.lastIndexOf('/'); 191 192 first = first.substring(0,index); 193 last = both.substring(index,both.length()); 194 exe = new File(_docRoot,first); 195 } 196 197 if (first.length() == 0 || !exe.exists() || exe.isDirectory() || !exe.getCanonicalPath().equals(exe.getAbsolutePath())) 198 { 199 res.sendError(404); 200 } 201 else 202 { 203 if (LOG.isDebugEnabled()) 204 { 205 LOG.debug("CGI: script is " + exe); 206 LOG.debug("CGI: pathInfo is " + last); 207 } 208 exec(exe,last,req,res); 209 } 210 } 211 212 /* ------------------------------------------------------------ */ 213 /* 214 * @param root @param path @param req @param res @exception IOException 215 */ 216 private void exec(File command, String pathInfo, HttpServletRequest req, HttpServletResponse res) throws IOException 217 { 218 String path = command.getAbsolutePath(); 219 File dir = command.getParentFile(); 220 String scriptName = req.getRequestURI().substring(0,req.getRequestURI().length() - pathInfo.length()); 221 String scriptPath = getServletContext().getRealPath(scriptName); 222 String pathTranslated = req.getPathTranslated(); 223 224 int len = req.getContentLength(); 225 if (len < 0) 226 len = 0; 227 if ((pathTranslated == null) || (pathTranslated.length() == 0)) 228 pathTranslated = path; 229 230 String bodyFormEncoded = null; 231 if ((HttpMethods.POST.equals(req.getMethod()) || HttpMethods.PUT.equals(req.getMethod())) && "application/x-www-form-urlencoded".equals(req.getContentType())) 232 { 233 MultiMap<String> parameterMap = new MultiMap<String>(); 234 Enumeration names = req.getParameterNames(); 235 while (names.hasMoreElements()) 236 { 237 String parameterName = (String)names.nextElement(); 238 parameterMap.addValues(parameterName, req.getParameterValues(parameterName)); 239 } 240 bodyFormEncoded = UrlEncoded.encode(parameterMap, req.getCharacterEncoding(), true); 241 } 242 243 EnvList env = new EnvList(_env); 244 // these ones are from "The WWW Common Gateway Interface Version 1.1" 245 // look at : 246 // http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1 247 env.set("AUTH_TYPE", req.getAuthType()); 248 if (bodyFormEncoded != null) 249 { 250 env.set("CONTENT_LENGTH", Integer.toString(bodyFormEncoded.length())); 251 } 252 else 253 { 254 env.set("CONTENT_LENGTH", Integer.toString(len)); 255 } 256 env.set("CONTENT_TYPE", req.getContentType()); 257 env.set("GATEWAY_INTERFACE", "CGI/1.1"); 258 if ((pathInfo != null) && (pathInfo.length() > 0)) 259 { 260 env.set("PATH_INFO", pathInfo); 261 } 262 env.set("PATH_TRANSLATED", pathTranslated); 263 env.set("QUERY_STRING", req.getQueryString()); 264 env.set("REMOTE_ADDR", req.getRemoteAddr()); 265 env.set("REMOTE_HOST", req.getRemoteHost()); 266 // The identity information reported about the connection by a 267 // RFC 1413 [11] request to the remote agent, if 268 // available. Servers MAY choose not to support this feature, or 269 // not to request the data for efficiency reasons. 270 // "REMOTE_IDENT" => "NYI" 271 env.set("REMOTE_USER", req.getRemoteUser()); 272 env.set("REQUEST_METHOD", req.getMethod()); 273 env.set("SCRIPT_NAME", scriptName); 274 env.set("SCRIPT_FILENAME", scriptPath); 275 env.set("SERVER_NAME", req.getServerName()); 276 env.set("SERVER_PORT", Integer.toString(req.getServerPort())); 277 env.set("SERVER_PROTOCOL", req.getProtocol()); 278 env.set("SERVER_SOFTWARE", getServletContext().getServerInfo()); 279 280 Enumeration enm = req.getHeaderNames(); 281 while (enm.hasMoreElements()) 282 { 283 String name = (String)enm.nextElement(); 284 String value = req.getHeader(name); 285 env.set("HTTP_" + name.toUpperCase(Locale.ENGLISH).replace('-','_'),value); 286 } 287 288 // these extra ones were from printenv on www.dev.nomura.co.uk 289 env.set("HTTPS", (req.isSecure()?"ON":"OFF")); 290 // "DOCUMENT_ROOT" => root + "/docs", 291 // "SERVER_URL" => "NYI - http://us0245", 292 // "TZ" => System.getProperty("user.timezone"), 293 294 // are we meant to decode args here ? or does the script get them 295 // via PATH_INFO ? if we are, they should be decoded and passed 296 // into exec here... 297 String execCmd = path; 298 if ((execCmd.charAt(0) != '"') && (execCmd.indexOf(" ") >= 0)) 299 execCmd = "\"" + execCmd + "\""; 300 if (_cmdPrefix != null) 301 execCmd = _cmdPrefix + " " + execCmd; 302 303 LOG.debug("Environment: " + env.getExportString()); 304 LOG.debug("Command: " + execCmd); 305 306 Process p; 307 if (dir == null) 308 p = Runtime.getRuntime().exec(execCmd, env.getEnvArray()); 309 else 310 p = Runtime.getRuntime().exec(execCmd, env.getEnvArray(), dir); 311 312 // hook processes input to browser's output (async) 313 if (bodyFormEncoded != null) 314 writeProcessInput(p, bodyFormEncoded); 315 else if (len > 0) 316 writeProcessInput(p, req.getInputStream(), len); 317 318 IO.copyThread(p.getErrorStream(), System.err); 319 320 // hook processes output to browser's input (sync) 321 // if browser closes stream, we should detect it and kill process... 322 OutputStream os = null; 323 try 324 { 325 // read any headers off the top of our input stream 326 // NOTE: Multiline header items not supported! 327 String line = null; 328 InputStream inFromCgi = p.getInputStream(); 329 330 // br=new BufferedReader(new InputStreamReader(inFromCgi)); 331 // while ((line=br.readLine())!=null) 332 while ((line = getTextLineFromStream(inFromCgi)).length() > 0) 333 { 334 if (!line.startsWith("HTTP")) 335 { 336 int k = line.indexOf(':'); 337 if (k > 0) 338 { 339 String key = line.substring(0,k).trim(); 340 String value = line.substring(k + 1).trim(); 341 if ("Location".equals(key)) 342 { 343 res.sendRedirect(res.encodeRedirectURL(value)); 344 } 345 else if ("Status".equals(key)) 346 { 347 String[] token = value.split(" "); 348 int status = Integer.parseInt(token[0]); 349 res.setStatus(status); 350 } 351 else 352 { 353 // add remaining header items to our response header 354 res.addHeader(key,value); 355 } 356 } 357 } 358 } 359 // copy cgi content to response stream... 360 os = res.getOutputStream(); 361 IO.copy(inFromCgi,os); 362 p.waitFor(); 363 364 if (!_ignoreExitState) 365 { 366 int exitValue = p.exitValue(); 367 if (0 != exitValue) 368 { 369 LOG.warn("Non-zero exit status (" + exitValue + ") from CGI program: " + path); 370 if (!res.isCommitted()) 371 res.sendError(500,"Failed to exec CGI"); 372 } 373 } 374 } 375 catch (IOException e) 376 { 377 // browser has probably closed its input stream - we 378 // terminate and clean up... 379 LOG.debug("CGI: Client closed connection!"); 380 } 381 catch (InterruptedException ie) 382 { 383 LOG.debug("CGI: interrupted!"); 384 } 385 finally 386 { 387 if (os != null) 388 { 389 try 390 { 391 os.close(); 392 } 393 catch (Exception e) 394 { 395 LOG.debug(e); 396 } 397 } 398 p.destroy(); 399 // LOG.debug("CGI: terminated!"); 400 } 401 } 402 403 private static void writeProcessInput(final Process p, final String input) 404 { 405 new Thread(new Runnable() 406 { 407 public void run() 408 { 409 try 410 { 411 Writer outToCgi = new OutputStreamWriter(p.getOutputStream()); 412 outToCgi.write(input); 413 outToCgi.close(); 414 } 415 catch (IOException e) 416 { 417 LOG.debug(e); 418 } 419 } 420 }).start(); 421 } 422 423 private static void writeProcessInput(final Process p, final InputStream input, final int len) 424 { 425 if (len <= 0) return; 426 427 new Thread(new Runnable() 428 { 429 public void run() 430 { 431 try 432 { 433 OutputStream outToCgi = p.getOutputStream(); 434 IO.copy(input, outToCgi, len); 435 outToCgi.close(); 436 } 437 catch (IOException e) 438 { 439 LOG.debug(e); 440 } 441 } 442 }).start(); 443 } 444 445 /** 446 * Utility method to get a line of text from the input stream. 447 * 448 * @param is 449 * the input stream 450 * @return the line of text 451 * @throws IOException 452 */ 453 private static String getTextLineFromStream(InputStream is) throws IOException 454 { 455 StringBuilder buffer = new StringBuilder(); 456 int b; 457 458 while ((b = is.read()) != -1 && b != '\n') 459 { 460 buffer.append((char)b); 461 } 462 return buffer.toString().trim(); 463 } 464 465 /* ------------------------------------------------------------ */ 466 /** 467 * private utility class that manages the Environment passed to exec. 468 */ 469 private static class EnvList 470 { 471 private Map<String, String> envMap; 472 473 EnvList() 474 { 475 envMap = new HashMap<String, String>(); 476 } 477 478 EnvList(EnvList l) 479 { 480 envMap = new HashMap<String,String>(l.envMap); 481 } 482 483 /** 484 * Set a name/value pair, null values will be treated as an empty String 485 */ 486 public void set(String name, String value) 487 { 488 envMap.put(name,name + "=" + StringUtil.nonNull(value)); 489 } 490 491 /** Get representation suitable for passing to exec. */ 492 public String[] getEnvArray() 493 { 494 return envMap.values().toArray(new String[envMap.size()]); 495 } 496 497 public String getExportString() 498 { 499 StringBuilder sb = new StringBuilder(); 500 for (String variable : getEnvArray()) 501 { 502 sb.append("export \""); 503 sb.append(variable); 504 sb.append("\"; "); 505 } 506 return sb.toString(); 507 } 508 509 @Override 510 public String toString() 511 { 512 return envMap.toString(); 513 } 514 } 515} 516