/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.server.dispatch; import com.caucho.config.ConfigException; import com.caucho.i18n.CharacterEncoding; import com.caucho.server.util.CauchoSystem; import com.caucho.util.CharBuffer; import com.caucho.util.FreeList; import com.caucho.util.L10N; import com.caucho.vfs.ByteToChar; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.logging.Level; import java.util.logging.Logger; /** * Decodes invocation URI. */ public class InvocationDecoder { private static final Logger log = Logger.getLogger(InvocationDecoder.class.getName()); private static final L10N L = new L10N(InvocationDecoder.class); private static final FreeList<ByteToChar> _freeConverters = new FreeList<ByteToChar>(256); // The character encoding private String _encoding = "UTF-8"; private String _sessionCookie = "JSESSIONID"; private String _sslSessionCookie; // The URL-encoded session suffix private String _sessionSuffix = ";jsessionid="; // The URL-encoded session prefix private String _sessionPrefix; private int _maxURILength = 1024; /** * Creates the invocation decoder. */ public InvocationDecoder() { _encoding = CharacterEncoding.getLocalEncoding(); if (_encoding == null) _encoding = "UTF-8"; } /** * Returns the character encoding. */ public String getEncoding() { return _encoding; } /** * Sets the character encoding. */ public void setEncoding(String encoding) { _encoding = encoding; } /** * Sets the session cookie */ public void setSessionCookie(String cookie) { _sessionCookie = cookie; } /** * Gets the session cookie */ public String getSessionCookie() { return _sessionCookie; } /** * Sets the SSL session cookie */ public void setSSLSessionCookie(String cookie) { _sslSessionCookie = cookie; } /** * Gets the SSL session cookie */ public String getSSLSessionCookie() { if (_sslSessionCookie != null) return _sslSessionCookie; else return _sessionCookie; } /** * Sets the session url prefix. */ public void setSessionURLPrefix(String prefix) { _sessionSuffix = prefix; } /** * Gets the session url prefix. */ public String getSessionURLPrefix() { return _sessionSuffix; } /** * Sets the alternate session url prefix. */ public void setAlternateSessionURLPrefix(String prefix) throws ConfigException { if (! prefix.startsWith("/")) prefix = '/' + prefix; if (prefix.lastIndexOf('/') > 0) throw new ConfigException(L.l("`{0}' is an invalidate alternate-session-url-prefix. The url-prefix must not have any embedded '/'.", prefix)); _sessionPrefix = prefix; _sessionSuffix = null; } /** * Gets the session url prefix. */ public String getAlternateSessionURLPrefix() { return _sessionPrefix; } public int getMaxURILength() { return _maxURILength; } public void setMaxURILength(int maxURILength) { _maxURILength = maxURILength; } /** * Splits out the query string and unescape the value. */ public void splitQueryAndUnescape(Invocation invocation, byte []rawURI, int uriLength) throws IOException { for (int i = 0; i < uriLength; i++) { if (rawURI[i] == '?') { i++; // XXX: should be the host encoding? String queryString = byteToChar(rawURI, i, uriLength - i, "ISO-8859-1"); invocation.setQueryString(queryString); uriLength = i - 1; break; } } String rawURIString = byteToChar(rawURI, 0, uriLength, "ISO-8859-1"); invocation.setRawURI(rawURIString); String decodedURI = normalizeUriEscape(rawURI, 0, uriLength, _encoding); if (_sessionSuffix != null) { int p = decodedURI.indexOf(_sessionSuffix); if (p >= 0) { int suffixLength = _sessionSuffix.length(); int tail = decodedURI.indexOf(';', p + suffixLength); String sessionId; if (tail > 0) sessionId = decodedURI.substring(p + suffixLength, tail); else sessionId = decodedURI.substring(p + suffixLength); decodedURI = decodedURI.substring(0, p); invocation.setSessionId(sessionId); p = rawURIString.indexOf(_sessionSuffix); if (p > 0) { rawURIString = rawURIString.substring(0, p); invocation.setRawURI(rawURIString); } } } else if (_sessionPrefix != null) { if (decodedURI.startsWith(_sessionPrefix)) { int prefixLength = _sessionPrefix.length(); int tail = decodedURI.indexOf('/', prefixLength); String sessionId; if (tail > 0) { sessionId = decodedURI.substring(prefixLength, tail); decodedURI = decodedURI.substring(tail); invocation.setRawURI(rawURIString.substring(tail)); } else { sessionId = decodedURI.substring(prefixLength); decodedURI = "/"; invocation.setRawURI("/"); } invocation.setSessionId(sessionId); } } String uri = normalizeUri(decodedURI); invocation.setURI(uri); invocation.setContextURI(uri); } /** * Splits out the query string, and normalizes the URI, assuming nothing * needs unescaping. */ public void splitQuery(Invocation invocation, String rawURI) throws IOException { int p = rawURI.indexOf('?'); if (p > 0) { invocation.setQueryString(rawURI.substring(p + 1)); rawURI = rawURI.substring(0, p); } invocation.setRawURI(rawURI); String uri = normalizeUri(rawURI); invocation.setURI(uri); invocation.setContextURI(uri); } /** * Just normalize the URI. */ public void normalizeURI(Invocation invocation, String rawURI) throws IOException { invocation.setRawURI(rawURI); String uri = normalizeUri(rawURI); invocation.setURI(uri); invocation.setContextURI(uri); } private String byteToChar(byte []buffer, int offset, int length, String encoding) { ByteToChar converter = allocateConverter(); // XXX: make this configurable if (encoding == null) encoding = "utf-8"; try { converter.setEncoding(encoding); } catch (UnsupportedEncodingException e) { log.log(Level.FINE, e.toString(), e); } String result; try { for (; length > 0; length--) converter.addByte(buffer[offset++]); result = converter.getConvertedString(); freeConverter(converter); } catch (IOException e) { result = "unknown"; } return result; } /** * Normalize a uri to remove '///', '/./', 'foo/..', etc. * * @param uri the raw uri to be normalized * @return a normalized URI */ public String normalizeUri(String uri) throws IOException { return normalizeUri(uri, CauchoSystem.isWindows()); } /** * Normalize a uri to remove '///', '/./', 'foo/..', etc. * * @param uri the raw uri to be normalized * @return a normalized URI */ public String normalizeUri(String uri, boolean isWindows) throws IOException { CharBuffer cb = new CharBuffer(); int len = uri.length(); if (len > _maxURILength) throw new BadRequestException(L.l("The request contains an illegal URL.")); char ch; if (len == 0 || (ch = uri.charAt(0)) != '/' && ch != '\\') cb.append('/'); for (int i = 0; i < len; i++) { ch = uri.charAt(i); if (ch == '/' || ch == '\\') { dots: while (i + 1 < len) { ch = uri.charAt(i + 1); if (ch == '/' || ch == '\\') i++; else if (ch != '.') break dots; else if (len <= i + 2 || (ch = uri.charAt(i + 2)) == '/' || ch == '\\') { i += 2; } else if (ch != '.') break dots; else if (len <= i + 3 || (ch = uri.charAt(i + 3)) == '/' || ch == '\\') { int j; for (j = cb.length() - 1; j >= 0; j--) { if ((ch = cb.charAt(j)) == '/' || ch == '\\') break; } if (j > 0) cb.setLength(j); else cb.setLength(0); i += 3; } else { throw new BadRequestException(L.l("The request contains an illegal URL.")); } } while (isWindows && cb.getLength() > 0 && ((ch = cb.getLastChar()) == '.' || ch == ' ')) { cb.setLength(cb.getLength() - 1); if (cb.getLength() > 0 && (ch = cb.getLastChar()) == '/' || ch == '\\') { cb.setLength(cb.getLength() - 1); // server/003n continue; } } cb.append('/'); } else if (ch == 0) throw new BadRequestException(L.l("The request contains an illegal URL.")); else cb.append(ch); } while (isWindows && cb.getLength() > 0 && ((ch = cb.getLastChar()) == '.' || ch == ' ')) { cb.setLength(cb.getLength() - 1); } return cb.toString(); } /** * Converts the escaped URI to a string. * * @param rawUri the escaped URI * @param i index into the URI * @param len the length of the uri * @param encoding the character encoding to handle %xx * * @return the converted URI */ private static String normalizeUriEscape(byte []rawUri, int i, int len, String encoding) throws IOException { ByteToChar converter = allocateConverter(); // XXX: make this configurable if (encoding == null) encoding = "utf-8"; try { converter.setEncoding(encoding); } catch (UnsupportedEncodingException e) { log.log(Level.FINE, e.toString(), e); } try { while (i < len) { int ch = rawUri[i++] & 0xff; if (ch == '%') i = scanUriEscape(converter, rawUri, i, len); else converter.addByte(ch); } String result = converter.getConvertedString(); freeConverter(converter); return result; } catch (Exception e) { throw new BadRequestException(L.l("The URL contains escaped bytes unsupported by the {0} encoding.", encoding)); } } /** * Scans the next character from URI, adding it to the converter. * * @param converter the byte-to-character converter * @param rawUri the raw URI * @param i index into the URI * @param len the raw URI length * * @return next index into the URI */ private static int scanUriEscape(ByteToChar converter, byte []rawUri, int i, int len) throws IOException { int ch1 = i < len ? (rawUri[i++] & 0xff) : -1; if (ch1 == 'u') { ch1 = i < len ? (rawUri[i++] & 0xff) : -1; int ch2 = i < len ? (rawUri[i++] & 0xff) : -1; int ch3 = i < len ? (rawUri[i++] & 0xff) : -1; int ch4 = i < len ? (rawUri[i++] & 0xff) : -1; converter.addChar((char) ((toHex(ch1) << 12) + (toHex(ch2) << 8) + (toHex(ch3) << 4) + (toHex(ch4)))); } else { int ch2 = i < len ? (rawUri[i++] & 0xff) : -1; int b = (toHex(ch1) << 4) + toHex(ch2);; converter.addByte(b); } return i; } /** * Convert a character to hex */ private static int toHex(int ch) { if (ch >= '0' && ch <= '9') return ch - '0'; else if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; else if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; else return -1; } private static ByteToChar allocateConverter() { ByteToChar converter = _freeConverters.allocate(); if (converter == null) converter = ByteToChar.create(); else converter.clear(); return converter; } private static void freeConverter(ByteToChar converter) { _freeConverters.free(converter); } }