001: /******************************************************************************
002: * LZHttpUtils.java
003: * ****************************************************************************/package org.openlaszlo.utils;
004:
005: import java.io.File;
006: import java.io.UnsupportedEncodingException;
007: import java.net.URL;
008: import javax.servlet.ServletContext;
009: import javax.servlet.http.HttpUtils;
010: import javax.servlet.http.HttpServletRequest;
011: import javax.servlet.http.HttpServletResponse;
012: import org.apache.commons.httpclient.methods.*;
013: import org.apache.commons.httpclient.HttpMethodBase;
014: import org.apache.commons.httpclient.Header;
015: import org.apache.commons.httpclient.URI;
016: import org.apache.commons.httpclient.URIException;
017:
018: import java.text.SimpleDateFormat;
019: import java.util.TimeZone;
020: import java.util.Date;
021: import java.util.Enumeration;
022: import java.security.*;
023:
024: import org.apache.log4j.*;
025:
026: import org.openlaszlo.utils.ChainedException;
027:
028: /**
029: * Utility class for http servlets
030: */
031: public class LZHttpUtils {
032:
033: private static Logger mLogger = Logger.getLogger(LZHttpUtils.class);
034:
035: public static final String CONTENT_ENCODING = "Content-Encoding";
036: public static final String CONTENT_LENGTH = "Content-Length";
037: public static final String CONTENT_TYPE = "Content-Type";
038: public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
039: public static final String LAST_MODIFIED = "Last-Modified";
040: public static final String IF_NONE_MATCH = "If-None-Match";
041: public static final String TRANSFER_ENCODING = "Transfer-Encoding";
042: public static final String HOST = "Host";
043: public static final String CONNECTION = "Connection";
044: public static final String AUTHORIZATION = "Authorization";
045: public static final String COOKIE = "Cookie";
046: public static final String CACHE_CONTROL = "Cache-Control";
047: public static final String USER_AGENT = "User-Agent";
048: public static final String ACCEPT_ENCODING = "Accept-Encoding";
049: public static final String RANGE = "Range";
050: public static final String ACCEPT_RANGES = "Accept-Ranges";
051: public static final String IF_RANGE = "If-Range";
052:
053: public static final String NO_STORE = "no-store";
054: public static final String NO_CACHE = "no-cache";
055:
056: /**
057: * @return the URL for the request // with the query string?
058: * @param req the request
059: */
060: public static URL getRequestURL(HttpServletRequest req) {
061: StringBuffer surl = HttpUtils.getRequestURL(req);
062: String sturl = surl.toString(); // I love java!
063: if (sturl.indexOf("https") == 0) {
064: try {
065: System.setProperty("java.protocol.handler.pkgs",
066: "com.sun.net.ssl.internal.www.protocol");
067: Class provClass = Class
068: .forName("com.sun.net.ssl.internal.ssl.Provider");
069: Provider provider = (Provider) provClass.newInstance();
070: Security.addProvider(provider);
071: } catch (InstantiationException e) {
072: throw new ChainedException(e);
073: } catch (IllegalAccessException e) {
074: throw new ChainedException(e);
075: } catch (ClassNotFoundException e) {
076: throw new ChainedException(e);
077: }
078: }
079: // surl.append("?");
080: // surl.append(req.getQueryString());
081: try {
082: return new URL(surl.toString());
083: } catch (Exception e) {
084: throw new ChainedException(e);
085: }
086: }
087:
088: /**
089: * For formatting HTTP dates
090: */
091:
092: /**
093: * Return a formatter for HTTP date headers
094: */
095: private static SimpleDateFormat getGMTFormatter() {
096: SimpleDateFormat dateFormatter = new SimpleDateFormat(
097: "EEE, dd MMM yyyy HH:mm:ss z", java.util.Locale.US);
098: TimeZone tz = TimeZone.getTimeZone("GMT");
099: dateFormatter.setTimeZone(tz);
100: return dateFormatter;
101: }
102:
103: /**
104: * Convert a long utc value to an HTTP Date String (TZ must be GMT)
105: */
106: public static String getDateString(long d) {
107: SimpleDateFormat dateFormatter = getGMTFormatter();
108: return dateFormatter.format(new Date(d));
109: }
110:
111: /**
112: * Convert an HTTP Date String to a long
113: * @return the long or -1 if the string doesn't parse correctly.
114: */
115: public static long getDate(String s) {
116: if (s == null || "".equals(s)) {
117: return -1;
118: }
119: try {
120: SimpleDateFormat dateFormatter = getGMTFormatter();
121: return dateFormatter.parse(s).getTime();
122: } catch (java.text.ParseException e) {
123: mLogger.warn(
124: /* (non-Javadoc)
125: * @i18n.test
126: * @org-mes="bad date string"
127: */
128: org.openlaszlo.i18n.LaszloMessages.getMessage(
129: LZHttpUtils.class.getName(), "051018-136"), e);
130: return -1;
131: }
132: }
133:
134: /** From RFC2616, 14.10:
135: *
136: * HTTP/1.1 proxies MUST parse the Connection header field before a message
137: * is forwarded and, for each connection-token in this field, remove any
138: * header field(s) from the message with the same name as the
139: * connection-token. Connection options are signaled by the presence of a
140: * connection-token in the Connection header field, not by any corresponding
141: * additional header field(s), since the additional header field may not be
142: * sent if there are no parameters associated with that connection
143: * option. */
144: static public boolean allowForward(String header,
145: Enumeration connEnum) {
146: if (header.toLowerCase().startsWith("content-"))
147: return false;
148:
149: if (header.equalsIgnoreCase(CONNECTION))
150: return false;
151:
152: if (header.equalsIgnoreCase(HOST))
153: return false;
154:
155: if (header.equalsIgnoreCase(TRANSFER_ENCODING))
156: return false;
157:
158: if (header.equalsIgnoreCase(IF_MODIFIED_SINCE))
159: return false;
160:
161: if (header.equalsIgnoreCase(LAST_MODIFIED))
162: return false;
163:
164: if (header.equalsIgnoreCase(IF_NONE_MATCH))
165: return false;
166:
167: if (header.equalsIgnoreCase(ACCEPT_ENCODING))
168: return false;
169:
170: // Someday we may allow these only when the proxy is non-transcoding
171: if (header.equalsIgnoreCase(RANGE))
172: return false;
173:
174: // Someday we may allow these only when the proxy is non-transcoding
175: if (header.equalsIgnoreCase(ACCEPT_RANGES))
176: return false;
177:
178: // Someday we may allow these only when the proxy is non-transcoding
179: if (header.equalsIgnoreCase(IF_RANGE))
180: return false;
181:
182: // Don't forward any headers that have the same name as a connection
183: // token.
184: if (connEnum != null) {
185: while (connEnum.hasMoreElements()) {
186: String token = (String) connEnum.nextElement();
187: if (header.equalsIgnoreCase(token))
188: return false;
189: }
190: }
191:
192: return true;
193: }
194:
195: /** Add request headers into method.
196: *
197: * @param req http servlet request object
198: * @param method method to insert request headers into
199: */
200: static public void proxyRequestHeaders(HttpServletRequest req,
201: HttpMethodBase method) {
202: mLogger.debug("proxyRequestHeaders");
203:
204: // Copy all headers, if the servlet container allows, otherwise just
205: // copy the cookie header and log a message.
206: Enumeration headerNames = req.getHeaderNames();
207: if (headerNames != null) {
208:
209: // Connection header tokens not to forward
210: Enumeration connEnum = req.getHeaders(CONNECTION);
211:
212: while (headerNames.hasMoreElements()) {
213: String key = (String) headerNames.nextElement();
214: if (allowForward(key, connEnum)) {
215: String val = (String) req.getHeader(key);
216: method.addRequestHeader(key, val);
217: mLogger.debug(" " + key + "=" + val);
218: }
219: }
220:
221: } else {
222: mLogger.warn(
223: /* (non-Javadoc)
224: * @i18n.test
225: * @org-mes="Can't get header names to proxy request headers"
226: */
227: org.openlaszlo.i18n.LaszloMessages.getMessage(
228: LZHttpUtils.class.getName(), "051018-237"));
229:
230: Enumeration cookieEnum = req.getHeaders(COOKIE);
231: if (cookieEnum != null) {
232: while (cookieEnum.hasMoreElements()) {
233: String val = (String) cookieEnum.nextElement();
234: method.addRequestHeader(COOKIE, val);
235: mLogger.debug(" Cookie=" + val);
236: }
237: }
238:
239: Enumeration authEnum = req.getHeaders(AUTHORIZATION);
240: if (authEnum != null) {
241: while (authEnum.hasMoreElements()) {
242: String val = (String) authEnum.nextElement();
243: method.addRequestHeader(AUTHORIZATION, val);
244: mLogger.debug(" Authorization=" + val);
245: }
246: }
247: }
248: }
249:
250: /** Pull response headers from method and put into
251: * servlet response object.
252: *
253: * @param method method to proxy from
254: * @param res http servlet response object to proxy to
255: * @param isSecure true if get method is secure
256: */
257: static public void proxyResponseHeaders(HttpMethodBase meth,
258: HttpServletResponse res, boolean isSecure) {
259: mLogger.debug("proxyResponseHeaders");
260:
261: Header[] hedz = meth.getResponseHeaders();
262:
263: for (int i = 0; i < hedz.length; i++) {
264: String name = hedz[i].getName();
265: String value = hedz[i].getValue();
266: // Content length passed back to swf app will be different
267: if (allowForward(name, null)) {
268: // Don't send no-cache headers request is SSL; IE 6 has
269: // problems.
270: if (isSecure) {
271: if (name.equals("Pragma")
272: && value.equals("no-cache"))
273: continue;
274: if (name.equals("Cache-Control")
275: && value.equals("no-cache"))
276: continue;
277: }
278:
279: mLogger.debug(" " + name + "=" + value);
280:
281: try {
282: if (name.equals("Date") || name.equals("Server")) {
283: res.setHeader(name, value);
284: } else {
285: res.addHeader(name, value);
286: }
287: } catch (Exception e) {
288: mLogger.error(
289: /* (non-Javadoc)
290: * @i18n.test
291: * @org-mes="Exception when proxying a response header: " + p[0]
292: */
293: org.openlaszlo.i18n.LaszloMessages.getMessage(
294: LZHttpUtils.class.getName(), "051018-304",
295: new Object[] { e.getMessage() }));
296: }
297: }
298: }
299: }
300:
301: /**
302: * Fetch cookie value for a particular cookie name.
303: *
304: * @param req servlet request.
305: * @param name name of cookie key to fetch.
306: */
307: static public String getCookie(HttpServletRequest req, String name) {
308: javax.servlet.http.Cookie[] cookies = req.getCookies();
309: if (cookies != null) {
310: for (int i = 0; i < cookies.length; i++) {
311: javax.servlet.http.Cookie cookie = cookies[i];
312: if (cookie.getName().equals(name)) {
313: return cookie.getValue();
314: }
315: }
316: }
317: return null;
318: }
319:
320: /**
321: * Replace real path forward slash characters to back-slash for Windoze.
322: * This is to get around a WebSphere problem (see bug 988). Note that if the
323: * web application content is being served directly from a .war file, this
324: * method will return null. See ServletContext.getRealPath() for more
325: * details.
326: *
327: * @param ctxt servlet context
328: * @param path virtual webapp path to resolve into a real path
329: * @return the real path, or null if the translation cannot be performed
330: */
331: static public String getRealPath(ServletContext ctxt, String path) {
332: String realPath = ctxt.getRealPath(path);
333: if (realPath != null && File.separatorChar == '\\')
334: realPath = realPath.replace('/', '\\');
335: try {
336: return new File(realPath).getCanonicalPath();
337: } catch (java.io.IOException e) {
338: throw new org.openlaszlo.utils.ChainedException(e);
339: }
340: }
341:
342: /**
343: * Replace real path forward slash characters to back-slash for Windoze.
344: *
345: * @param ctxt servlet context
346: * @param req generating request
347: * @return the real path, or null if the translation cannot be performed
348: */
349: static public String getRealPath(ServletContext ctxt,
350: HttpServletRequest req) {
351: String uriwithoutcontext = req.getRequestURI().substring(
352: req.getContextPath().length());
353: if (uriwithoutcontext != null && File.separatorChar == '\\') {
354: uriwithoutcontext = uriwithoutcontext.replace('/', '\\');
355: }
356: return getRealPath(ctxt, "/") + uriwithoutcontext;
357: }
358:
359: private static String WEBAPP = "/@WEBAPP@/";
360:
361: /**
362: * If a URL contains <code>/@WEBAPP@</code>, replaces that string with
363: * context path. If context path is <code>/</code>, the function just
364: * removes the <code>/@WEBAPP@</code> string.
365: *
366: * @param url URL to check if <code>/@WEBAPP@</code> token exists.
367: * @return if <code>/@WEBAPP@</code> exists, new modified URL else old URL.
368: */
369: public static String modifyWEBAPP(HttpServletRequest req, String url) {
370: mLogger.debug("modifyWEBAPP");
371: if (url.startsWith(WEBAPP)) {
372: mLogger.debug(" Old URL: " + url);
373: String protocol = (req.isSecure() ? "https" : "http");
374: String host = req.getServerName();
375: int port = req.getServerPort();
376: String cp = req.getContextPath();
377: url = protocol + "://" + host + ":" + port + cp
378: + url.substring(WEBAPP.length() - 1);
379: mLogger.debug(" New URL: " + url);
380: }
381: return url;
382: }
383:
384: /**
385: * Mark response with no-store cache control
386: */
387: static public void noStore(HttpServletResponse res) {
388: if (res.containsHeader(LZHttpUtils.CACHE_CONTROL)) {
389: mLogger.warn(
390: /* (non-Javadoc)
391: * @i18n.test
392: * @org-mes="over-riding back-end cache-control header to: no-store"
393: */
394: org.openlaszlo.i18n.LaszloMessages.getMessage(
395: LZHttpUtils.class.getName(), "051018-408"));
396: }
397: res.setHeader(CACHE_CONTROL, NO_STORE);
398: }
399:
400: /**
401: * Return a URI object, escaping input only if needed
402: */
403: static public URI newURI(String s) throws URIException {
404: try {
405: return new URI(s, true);
406: } catch (URIException urie) {
407: // Try escaping
408: try {
409: return new URI(s, false);
410: } catch (Exception e) {
411: // Escaping failed, throw the original error
412: throw urie;
413: }
414: }
415: }
416:
417: /**
418: * Decodes a urlencoded string using a specific charset encoding.
419: */
420: public static String urldecode(String s, String enc)
421: throws UnsupportedEncodingException {
422: boolean needToChange = false;
423: StringBuffer sb = new StringBuffer();
424: int numChars = s.length();
425: int i = 0;
426:
427: if (enc.length() == 0) {
428: throw new UnsupportedEncodingException(
429: /* (non-Javadoc)
430: * @i18n.test
431: * @org-mes="LzHTTPUtils.urldecode: empty string enc parameter"
432: */
433: org.openlaszlo.i18n.LaszloMessages.getMessage(
434: LZHttpUtils.class.getName(), "051018-449"));
435: }
436:
437: while (i < numChars) {
438: char c = s.charAt(i);
439: switch (c) {
440: case '+':
441: sb.append(' ');
442: i++;
443: needToChange = true;
444: break;
445: case '%':
446: /*
447: * Starting with this instance of %, process all
448: * consecutive substrings of the form %xy. Each
449: * substring %xy will yield a byte. Convert all
450: * consecutive bytes obtained this way to whatever
451: * character(s) they represent in the provided
452: * encoding.
453: */
454:
455: try {
456:
457: // (numChars-i)/3 is an upper bound for the number
458: // of remaining bytes
459: byte[] bytes = new byte[(numChars - i) / 3];
460: int pos = 0;
461:
462: while (((i + 2) < numChars) && (c == '%')) {
463: bytes[pos++] = (byte) Integer.parseInt(s
464: .substring(i + 1, i + 3), 16);
465: i += 3;
466: if (i < numChars)
467: c = s.charAt(i);
468: }
469:
470: // A trailing, incomplete byte encoding such as
471: // "%x" will cause an exception to be thrown
472:
473: if ((i < numChars) && (c == '%'))
474: throw new IllegalArgumentException(
475: /* (non-Javadoc)
476: * @i18n.test
477: * @org-mes="URLDecoder: Incomplete trailing escape (%) pattern"
478: */
479: org.openlaszlo.i18n.LaszloMessages.getMessage(
480: LZHttpUtils.class.getName(),
481: "051018-497"));
482:
483: sb.append(new String(bytes, 0, pos, enc));
484: } catch (NumberFormatException e) {
485: throw new IllegalArgumentException(
486: /* (non-Javadoc)
487: * @i18n.test
488: * @org-mes="URLDecoder: Illegal hex characters in escape (%) pattern - " + p[0]
489: */
490: org.openlaszlo.i18n.LaszloMessages.getMessage(
491: LZHttpUtils.class.getName(), "051018-508",
492: new Object[] { e.getMessage() }));
493: }
494: needToChange = true;
495: break;
496: default:
497: sb.append(c);
498: i++;
499: break;
500: }
501: }
502: return (needToChange ? sb.toString() : s);
503: }
504:
505: }
|