| /* |
| * Copyright 1999-2004 The Apache Software Foundation |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.coyote.tomcat4; |
| |
| |
| import java.io.IOException; |
| import javax.servlet.http.Cookie; |
| import javax.servlet.http.HttpServletRequest; |
| |
| import org.apache.tomcat.util.buf.B2CConverter; |
| import org.apache.tomcat.util.buf.ByteChunk; |
| import org.apache.tomcat.util.buf.CharChunk; |
| import org.apache.tomcat.util.buf.MessageBytes; |
| import org.apache.tomcat.util.http.Cookies; |
| import org.apache.tomcat.util.http.ServerCookie; |
| |
| import org.apache.coyote.ActionCode; |
| import org.apache.coyote.Adapter; |
| import org.apache.coyote.Request; |
| import org.apache.coyote.Response; |
| |
| import org.apache.catalina.Globals; |
| import org.apache.catalina.Logger; |
| import org.apache.catalina.util.StringManager; |
| |
| |
| /** |
| * Implementation of a request processor which delegates the processing to a |
| * Coyote processor. |
| * |
| * @author Craig R. McClanahan |
| * @author Remy Maucherat |
| * @version $Revision$ $Date$ |
| */ |
| |
| final class CoyoteAdapter |
| implements Adapter { |
| |
| |
| // -------------------------------------------------------------- Constants |
| |
| |
| public static final int ADAPTER_NOTES = 1; |
| |
| |
| // ----------------------------------------------------------- Constructors |
| |
| |
| /** |
| * Construct a new CoyoteProcessor associated with the specified connector. |
| * |
| * @param connector CoyoteConnector that owns this processor |
| * @param id Identifier of this CoyoteProcessor (unique per connector) |
| */ |
| public CoyoteAdapter(CoyoteConnector connector) { |
| |
| super(); |
| this.connector = connector; |
| this.debug = connector.getDebug(); |
| |
| } |
| |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| |
| /** |
| * The CoyoteConnector with which this processor is associated. |
| */ |
| private CoyoteConnector connector = null; |
| |
| |
| /** |
| * The debugging detail level for this component. |
| */ |
| private int debug = 0; |
| |
| |
| /** |
| * The match string for identifying a session ID parameter. |
| */ |
| private static final String match = |
| ";" + Globals.SESSION_PARAMETER_NAME + "="; |
| |
| |
| /** |
| * The match string for identifying a session ID parameter. |
| */ |
| private static final char[] SESSION_ID = match.toCharArray(); |
| |
| |
| /** |
| * The string manager for this package. |
| */ |
| protected StringManager sm = |
| StringManager.getManager(Constants.Package); |
| |
| |
| // -------------------------------------------------------- Adapter Methods |
| |
| |
| /** |
| * Service method. |
| */ |
| public void service(Request req, Response res) |
| throws Exception { |
| |
| CoyoteRequest request = (CoyoteRequest) req.getNote(ADAPTER_NOTES); |
| CoyoteResponse response = (CoyoteResponse) res.getNote(ADAPTER_NOTES); |
| |
| if (request == null) { |
| |
| // Create objects |
| request = (CoyoteRequest) connector.createRequest(); |
| request.setCoyoteRequest(req); |
| response = (CoyoteResponse) connector.createResponse(); |
| response.setCoyoteResponse(res); |
| |
| // Link objects |
| request.setResponse(response); |
| response.setRequest(request); |
| |
| // Set as notes |
| req.setNote(ADAPTER_NOTES, request); |
| res.setNote(ADAPTER_NOTES, response); |
| |
| // Set query string encoding |
| req.getParameters().setQueryStringEncoding |
| (connector.getURIEncoding()); |
| |
| } |
| |
| try { |
| // Parse and set Catalina and configuration specific |
| // request parameters |
| postParseRequest(req, request, res, response); |
| // Calling the container |
| connector.getContainer().invoke(request, response); |
| response.finishResponse(); |
| |
| req.action( ActionCode.ACTION_POST_REQUEST , null); |
| } catch (IOException e) { |
| ; |
| } catch (Throwable t) { |
| log(sm.getString("coyoteAdapter.service"), t); |
| } finally { |
| // Recycle the wrapper request and response |
| request.recycle(); |
| response.recycle(); |
| } |
| |
| } |
| |
| |
| // ------------------------------------------------------ Protected Methods |
| |
| |
| /** |
| * Parse additional request parameters. |
| */ |
| protected void postParseRequest(Request req, CoyoteRequest request, |
| Response res, CoyoteResponse response) |
| throws Exception { |
| // XXX the processor needs to set a correct scheme and port prior to this point, |
| // in ajp13 protocols dont make sense to get the port from the connector.. |
| // XXX the processor may have set a correct scheme and port prior to this point, |
| // in ajp13 protocols dont make sense to get the port from the connector... |
| // otherwise, use connector configuration |
| if (! req.scheme().isNull()) { |
| // use processor specified scheme to determine secure state |
| request.setSecure(req.scheme().equals("https")); |
| } else { |
| // use connector scheme and secure configuration, (defaults to |
| // "http" and false respectively) |
| req.scheme().setString(connector.getScheme()); |
| request.setSecure(connector.getSecure()); |
| } |
| |
| // Filter trace method |
| if (!connector.getAllowTrace() |
| && req.method().equalsIgnoreCase("TRACE")) { |
| res.setStatus(403); |
| res.setMessage("TRACE method is not allowed"); |
| throw new IOException("TRACE method is not allowed"); |
| } |
| |
| request.setAuthorization |
| (req.getHeader(Constants.AUTHORIZATION_HEADER)); |
| // FIXME: the code below doesnt belongs to here, this is only have sense |
| // in Http11, not in ajp13.. |
| // At this point the Host header has been processed. |
| // Override if the proxyPort/proxyHost are set |
| String proxyName = connector.getProxyName(); |
| int proxyPort = connector.getProxyPort(); |
| if (proxyPort != 0) { |
| request.setServerPort(proxyPort); |
| req.setServerPort(proxyPort); |
| } else { |
| request.setServerPort(req.getServerPort()); |
| } |
| if (proxyName != null) { |
| request.setServerName(proxyName); |
| req.serverName().setString(proxyName); |
| } else { |
| request.setServerName(req.serverName().toString()); |
| } |
| |
| // URI decoding |
| req.decodedURI().duplicate(req.requestURI()); |
| try { |
| req.getURLDecoder().convert(req.decodedURI(), false); |
| } catch (IOException ioe) { |
| res.setStatus(400); |
| res.setMessage("Invalid URI"); |
| throw ioe; |
| } |
| |
| // Normalize decoded URI |
| if (!normalize(req.decodedURI())) { |
| res.setStatus(400); |
| res.setMessage("Invalid URI"); |
| throw new IOException("Invalid URI"); |
| } |
| |
| // URI character decoding |
| convertURI(req.decodedURI(), request); |
| |
| // Parse session Id |
| parseSessionId(req, request); |
| |
| // Additional URI normalization and validation is needed for security |
| // reasons on Tomcat 4.0.x |
| if (connector.getUseURIValidationHack()) { |
| String uri = validate(request.getRequestURI()); |
| if (uri == null) { |
| res.setStatus(400); |
| res.setMessage("Invalid URI"); |
| throw new IOException("Invalid URI"); |
| } else { |
| req.requestURI().setString(uri); |
| // Redoing the URI decoding |
| req.decodedURI().duplicate(req.requestURI()); |
| req.getURLDecoder().convert(req.decodedURI(), true); |
| convertURI(req.decodedURI(), request); |
| } |
| } |
| |
| // Parse cookies |
| parseCookies(req, request); |
| |
| // Set the SSL properties |
| if( request.isSecure() ) { |
| res.action(ActionCode.ACTION_REQ_SSL_ATTRIBUTE, |
| request.getCoyoteRequest()); |
| //Set up for getAttributeNames |
| request.getAttribute(Globals.CERTIFICATES_ATTR); |
| request.getAttribute(Globals.CIPHER_SUITE_ATTR); |
| request.getAttribute(Globals.KEY_SIZE_ATTR); |
| } |
| |
| // Set the remote principal |
| String principal = req.getRemoteUser().toString(); |
| if (principal != null) { |
| request.setUserPrincipal(new CoyotePrincipal(principal)); |
| } |
| |
| // Set the authorization type |
| String authtype = req.getAuthType().toString(); |
| if (authtype != null) { |
| request.setAuthType(authtype); |
| } |
| |
| } |
| |
| /** |
| * Parse session id in URL. |
| * FIXME: Optimize this. |
| */ |
| protected void parseSessionId(Request req, CoyoteRequest request) { |
| |
| req.decodedURI().toChars(); |
| CharChunk uriCC = req.decodedURI().getCharChunk(); |
| int semicolon = uriCC.indexOf(match, 0, match.length(), 0); |
| |
| if (semicolon > 0) { |
| |
| // Parse session ID, and extract it from the decoded request URI |
| int start = uriCC.getStart(); |
| int end = uriCC.getEnd(); |
| |
| int sessionIdStart = start + semicolon + match.length(); |
| int semicolon2 = uriCC.indexOf(';', sessionIdStart); |
| if (semicolon2 >= 0) { |
| request.setRequestedSessionId |
| (new String(uriCC.getBuffer(), sessionIdStart, |
| semicolon2 - semicolon - match.length())); |
| req.decodedURI().setString |
| (new String(uriCC.getBuffer(), start, semicolon) + |
| new String(uriCC.getBuffer(), |
| semicolon2, |
| end-semicolon2)); |
| } else { |
| request.setRequestedSessionId |
| (new String(uriCC.getBuffer(), sessionIdStart, |
| end - sessionIdStart)); |
| req.decodedURI().setString |
| (new String(uriCC.getBuffer(), start, semicolon)); |
| } |
| request.setRequestedSessionURL(true); |
| |
| // Extract session ID from request URI |
| String uri = req.requestURI().toString(); |
| semicolon = uri.indexOf(match); |
| |
| if (semicolon > 0) { |
| String rest = uri.substring(semicolon + match.length()); |
| semicolon2 = rest.indexOf(';'); |
| if (semicolon2 >= 0) { |
| rest = rest.substring(semicolon2); |
| } else { |
| rest = ""; |
| } |
| req.requestURI().setString(uri.substring(0, semicolon) + rest); |
| } |
| |
| } else { |
| request.setRequestedSessionId(null); |
| request.setRequestedSessionURL(false); |
| } |
| |
| } |
| |
| |
| /** |
| * Parse cookies. |
| */ |
| protected void parseCookies(Request req, CoyoteRequest request) { |
| |
| Cookies serverCookies = req.getCookies(); |
| int count = serverCookies.getCookieCount(); |
| if (count <= 0) |
| return; |
| |
| Cookie[] cookies = new Cookie[count]; |
| |
| int idx=0; |
| for (int i = 0; i < count; i++) { |
| ServerCookie scookie = serverCookies.getCookie(i); |
| if (scookie.getName().equals(Globals.SESSION_COOKIE_NAME)) { |
| // Override anything requested in the URL |
| if (!request.isRequestedSessionIdFromCookie()) { |
| // Accept only the first session id cookie |
| request.setRequestedSessionId |
| (scookie.getValue().toString()); |
| request.setRequestedSessionCookie(true); |
| request.setRequestedSessionURL(false); |
| if (debug >= 1) |
| log(" Requested cookie session id is " + |
| ((HttpServletRequest) request.getRequest()) |
| .getRequestedSessionId()); |
| } |
| } |
| try { |
| Cookie cookie = new Cookie(scookie.getName().toString(), |
| scookie.getValue().toString()); |
| cookie.setPath(scookie.getPath().toString()); |
| cookie.setVersion(scookie.getVersion()); |
| String domain = scookie.getDomain().toString(); |
| if (domain != null) { |
| cookie.setDomain(scookie.getDomain().toString()); |
| } |
| cookies[idx++] = cookie; |
| } catch(Exception ex) { |
| log("Bad Cookie Name: " + scookie.getName() + |
| " /Value: " + scookie.getValue(),ex); |
| } |
| } |
| if( idx < count ) { |
| Cookie [] ncookies = new Cookie[idx]; |
| System.arraycopy(cookies, 0, ncookies, 0, idx); |
| cookies = ncookies; |
| } |
| |
| request.setCookies(cookies); |
| |
| } |
| |
| |
| /** |
| * Return a context-relative path, beginning with a "/", that represents |
| * the canonical version of the specified path after ".." and "." elements |
| * are resolved out. If the specified path attempts to go outside the |
| * boundaries of the current context (i.e. too many ".." path elements |
| * are present), return <code>null</code> instead. |
| * This code is not optimized, and is only needed for Tomcat 4.0.x. |
| * |
| * @param path Path to be validated |
| */ |
| protected static String validate(String path) { |
| |
| if (path == null) |
| return null; |
| |
| // Create a place for the normalized path |
| String normalized = path; |
| |
| // Normalize "/%7E" and "/%7e" at the beginning to "/~" |
| if (normalized.startsWith("/%7E") || |
| normalized.startsWith("/%7e")) |
| normalized = "/~" + normalized.substring(4); |
| |
| // Prevent encoding '%', '/', '.' and '\', which are special reserved |
| // characters |
| if ((normalized.indexOf("%25") >= 0) |
| || (normalized.indexOf("%2F") >= 0) |
| || (normalized.indexOf("%2E") >= 0) |
| || (normalized.indexOf("%5C") >= 0) |
| || (normalized.indexOf("%2f") >= 0) |
| || (normalized.indexOf("%2e") >= 0) |
| || (normalized.indexOf("%5c") >= 0)) { |
| return null; |
| } |
| |
| if (normalized.equals("/.")) |
| return "/"; |
| |
| // Normalize the slashes and add leading slash if necessary |
| if (normalized.indexOf('\\') >= 0) |
| normalized = normalized.replace('\\', '/'); |
| if (!normalized.startsWith("/")) |
| normalized = "/" + normalized; |
| |
| // Resolve occurrences of "//" in the normalized path |
| while (true) { |
| int index = normalized.indexOf("//"); |
| if (index < 0) |
| break; |
| normalized = normalized.substring(0, index) + |
| normalized.substring(index + 1); |
| } |
| |
| // Resolve occurrences of "/./" in the normalized path |
| while (true) { |
| int index = normalized.indexOf("/./"); |
| if (index < 0) |
| break; |
| normalized = normalized.substring(0, index) + |
| normalized.substring(index + 2); |
| } |
| |
| // Resolve occurrences of "/../" in the normalized path |
| while (true) { |
| int index = normalized.indexOf("/../"); |
| if (index < 0) |
| break; |
| if (index == 0) |
| return (null); // Trying to go outside our context |
| int index2 = normalized.lastIndexOf('/', index - 1); |
| normalized = normalized.substring(0, index2) + |
| normalized.substring(index + 3); |
| } |
| |
| // Declare occurrences of "/..." (three or more dots) to be invalid |
| // (on some Windows platforms this walks the directory tree!!!) |
| if (normalized.indexOf("/...") >= 0) |
| return (null); |
| |
| // Return the normalized path that we have completed |
| return (normalized); |
| |
| } |
| |
| |
| /** |
| * Character conversion of the URI. |
| */ |
| protected void convertURI(MessageBytes uri, CoyoteRequest request) |
| throws Exception { |
| |
| ByteChunk bc = uri.getByteChunk(); |
| CharChunk cc = uri.getCharChunk(); |
| cc.allocate(bc.getLength(), -1); |
| |
| String enc = connector.getURIEncoding(); |
| if (enc != null) { |
| B2CConverter conv = request.getURIConverter(); |
| try { |
| if (conv == null) { |
| conv = new B2CConverter(enc); |
| request.setURIConverter(conv); |
| } else { |
| conv.recycle(); |
| } |
| } catch (IOException e) { |
| // Ignore |
| log("Invalid URI encoding; using HTTP default", e); |
| connector.setURIEncoding(null); |
| } |
| if (conv != null) { |
| try { |
| conv.convert(bc, cc); |
| uri.setChars(cc.getBuffer(), cc.getStart(), |
| cc.getLength()); |
| return; |
| } catch (IOException e) { |
| if (debug >= 1) { |
| log("Invalid URI character encoding; trying ascii", e); |
| } |
| cc.recycle(); |
| } |
| } |
| } |
| |
| // Default encoding: fast conversion |
| byte[] bbuf = bc.getBuffer(); |
| char[] cbuf = cc.getBuffer(); |
| int start = bc.getStart(); |
| for (int i = 0; i < bc.getLength(); i++) { |
| cbuf[i] = (char) (bbuf[i + start] & 0xff); |
| } |
| uri.setChars(cbuf, 0, bc.getLength()); |
| |
| } |
| |
| |
| /** |
| * Normalize URI. |
| * <p> |
| * This method normalizes "\", "//", "/./" and "/../". This method will |
| * return false when trying to go above the root, or if the URI contains |
| * a null byte. |
| * |
| * @param uriMB URI to be normalized |
| */ |
| public static boolean normalize(MessageBytes uriMB) { |
| |
| ByteChunk uriBC = uriMB.getByteChunk(); |
| byte[] b = uriBC.getBytes(); |
| int start = uriBC.getStart(); |
| int end = uriBC.getEnd(); |
| |
| // URL * is acceptable |
| if ((end - start == 1) && b[start] == (byte) '*') |
| return true; |
| |
| int pos = 0; |
| int index = 0; |
| |
| // Replace '\' with '/' |
| // Check for null byte |
| for (pos = start; pos < end; pos++) { |
| if (b[pos] == (byte) '\\') |
| b[pos] = (byte) '/'; |
| if (b[pos] == (byte) 0) |
| return false; |
| } |
| |
| // The URL must start with '/' |
| if (b[start] != (byte) '/') { |
| return false; |
| } |
| |
| // Replace "//" with "/" |
| for (pos = start; pos < (end - 1); pos++) { |
| if (b[pos] == (byte) '/') { |
| while ((pos + 1 < end) && (b[pos + 1] == (byte) '/')) { |
| copyBytes(b, pos, pos + 1, end - pos - 1); |
| end--; |
| } |
| } |
| } |
| |
| // If the URI ends with "/." or "/..", then we append an extra "/" |
| // Note: It is possible to extend the URI by 1 without any side effect |
| // as the next character is a non-significant WS. |
| if (((end - start) > 2) && (b[end - 1] == (byte) '.')) { |
| if ((b[end - 2] == (byte) '/') |
| || ((b[end - 2] == (byte) '.') |
| && (b[end - 3] == (byte) '/'))) { |
| b[end] = (byte) '/'; |
| end++; |
| } |
| } |
| |
| uriBC.setEnd(end); |
| |
| index = 0; |
| |
| // Resolve occurrences of "/./" in the normalized path |
| while (true) { |
| index = uriBC.indexOf("/./", 0, 3, index); |
| if (index < 0) |
| break; |
| copyBytes(b, start + index, start + index + 2, |
| end - start - index - 2); |
| end = end - 2; |
| uriBC.setEnd(end); |
| } |
| |
| index = 0; |
| |
| // Resolve occurrences of "/../" in the normalized path |
| while (true) { |
| index = uriBC.indexOf("/../", 0, 4, index); |
| if (index < 0) |
| break; |
| // Prevent from going outside our context |
| if (index == 0) |
| return false; |
| int index2 = -1; |
| for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos --) { |
| if (b[pos] == (byte) '/') { |
| index2 = pos; |
| } |
| } |
| copyBytes(b, start + index2, start + index + 3, |
| end - start - index - 3); |
| end = end + index2 - index - 3; |
| uriBC.setEnd(end); |
| index = index2; |
| } |
| |
| uriBC.setBytes(b, start, end); |
| |
| return true; |
| |
| } |
| |
| |
| // ------------------------------------------------------ Protected Methods |
| |
| |
| /** |
| * Copy an array of bytes to a different position. Used during |
| * normalization. |
| */ |
| protected static void copyBytes(byte[] b, int dest, int src, int len) { |
| for (int pos = 0; pos < len; pos++) { |
| b[pos + dest] = b[pos + src]; |
| } |
| } |
| |
| |
| /** |
| * Log a message on the Logger associated with our Container (if any) |
| * |
| * @param message Message to be logged |
| */ |
| protected void log(String message) { |
| |
| Logger logger = connector.getContainer().getLogger(); |
| if (logger != null) |
| logger.log("CoyoteAdapter " + message); |
| |
| } |
| |
| |
| /** |
| * Log a message on the Logger associated with our Container (if any) |
| * |
| * @param message Message to be logged |
| * @param throwable Associated exception |
| */ |
| protected void log(String message, Throwable throwable) { |
| |
| Logger logger = connector.getContainer().getLogger(); |
| if (logger != null) |
| logger.log("CoyoteAdapter " + message, throwable); |
| |
| } |
| |
| |
| } |