CoyoteAdapter.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.catalina.connector;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.concurrent.atomic.AtomicBoolean;
import jakarta.servlet.ReadListener;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.SessionTrackingMode;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.Authenticator;
import org.apache.catalina.Context;
import org.apache.catalina.Host;
import org.apache.catalina.Wrapper;
import org.apache.catalina.authenticator.AuthenticatorBase;
import org.apache.catalina.core.AsyncContextImpl;
import org.apache.catalina.util.ServerInfo;
import org.apache.catalina.util.SessionConfig;
import org.apache.catalina.util.URLEncoder;
import org.apache.coyote.ActionCode;
import org.apache.coyote.Adapter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
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.HexUtils;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.ServerCookie;
import org.apache.tomcat.util.http.ServerCookies;
import org.apache.tomcat.util.net.SSLSupport;
import org.apache.tomcat.util.net.SocketEvent;
import org.apache.tomcat.util.res.StringManager;
/**
* Implementation of a request processor which delegates the processing to a Coyote processor.
*
* @author Craig R. McClanahan
* @author Remy Maucherat
*/
public class CoyoteAdapter implements Adapter {
private static final Log log = LogFactory.getLog(CoyoteAdapter.class);
// -------------------------------------------------------------- Constants
private static final String POWERED_BY = "Servlet/6.1 JSP/4.0 " + "(" + ServerInfo.getServerInfo() + " Java/" +
System.getProperty("java.vm.vendor") + "/" + System.getProperty("java.runtime.version") + ")";
private static final EnumSet<SessionTrackingMode> SSL_ONLY = EnumSet.of(SessionTrackingMode.SSL);
public static final int ADAPTER_NOTES = 1;
// ----------------------------------------------------------- Constructors
/**
* Construct a new CoyoteProcessor associated with the specified connector.
*
* @param connector CoyoteConnector that owns this processor
*/
public CoyoteAdapter(Connector connector) {
super();
this.connector = connector;
}
// ----------------------------------------------------- Instance Variables
/**
* The CoyoteConnector with which this processor is associated.
*/
private final Connector connector;
/**
* The string manager for this package.
*/
protected static final StringManager sm = StringManager.getManager(CoyoteAdapter.class);
// -------------------------------------------------------- Adapter Methods
@Override
public boolean asyncDispatch(org.apache.coyote.Request req, org.apache.coyote.Response res, SocketEvent status)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
throw new IllegalStateException(sm.getString("coyoteAdapter.nullRequest"));
}
boolean success = true;
AsyncContextImpl asyncConImpl = request.getAsyncContextInternal();
req.setRequestThread();
try {
if (!request.isAsync()) {
// Error or timeout
// Lift any suspension (e.g. if sendError() was used by an async
// request) to allow the response to be written to the client
response.setSuspended(false);
}
if (status == SocketEvent.TIMEOUT) {
if (!asyncConImpl.timeout()) {
asyncConImpl.setErrorState(null, false);
}
} else if (status == SocketEvent.ERROR) {
// An I/O error occurred on a non-container thread which means
// that the socket needs to be closed so set success to false to
// trigger a close
success = false;
Throwable t = (Throwable) req.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
Context context = request.getContext();
ClassLoader oldCL = null;
try {
oldCL = context.bind(null);
if (req.getReadListener() != null) {
req.getReadListener().onError(t);
}
if (res.getWriteListener() != null) {
res.getWriteListener().onError(t);
}
res.action(ActionCode.CLOSE_NOW, t);
asyncConImpl.setErrorState(t, true);
} finally {
context.unbind(oldCL);
}
}
// Check to see if non-blocking writes or reads are being used
if (!request.isAsyncDispatching() && request.isAsync()) {
WriteListener writeListener = res.getWriteListener();
ReadListener readListener = req.getReadListener();
if (writeListener != null && status == SocketEvent.OPEN_WRITE) {
Context context = request.getContext();
ClassLoader oldCL = null;
try {
oldCL = context.bind(null);
res.onWritePossible();
if (request.isFinished() && req.sendAllDataReadEvent() && readListener != null) {
readListener.onAllDataRead();
}
// User code may have swallowed an IOException
if (response.getCoyoteResponse().isExceptionPresent()) {
throw response.getCoyoteResponse().getErrorException();
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// Allow the error handling to write to the response
response.setSuspended(false);
// Need to trigger the call to AbstractProcessor.setErrorState()
// before the listener is called so the listener can call complete
// Therefore no need to set success=false as that would trigger a
// second call to AbstractProcessor.setErrorState()
// https://bz.apache.org/bugzilla/show_bug.cgi?id=65001
writeListener.onError(t);
res.action(ActionCode.CLOSE_NOW, t);
asyncConImpl.setErrorState(t, true);
} finally {
context.unbind(oldCL);
}
} else if (readListener != null && status == SocketEvent.OPEN_READ) {
Context context = request.getContext();
ClassLoader oldCL = null;
try {
oldCL = context.bind(null);
// If data is being read on a non-container thread a
// dispatch with status OPEN_READ will be used to get
// execution back on a container thread for the
// onAllDataRead() event. Therefore, make sure
// onDataAvailable() is not called in this case.
if (!request.isFinished()) {
req.onDataAvailable();
}
if (request.isFinished() && req.sendAllDataReadEvent()) {
readListener.onAllDataRead();
}
// User code may have swallowed an IOException
if (request.getCoyoteRequest().isExceptionPresent()) {
throw request.getCoyoteRequest().getErrorException();
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// Allow the error handling to write to the response
response.setSuspended(false);
// Need to trigger the call to AbstractProcessor.setErrorState()
// before the listener is called so the listener can call complete
// Therefore no need to set success=false as that would trigger a
// second call to AbstractProcessor.setErrorState()
// https://bz.apache.org/bugzilla/show_bug.cgi?id=65001
readListener.onError(t);
res.action(ActionCode.CLOSE_NOW, t);
asyncConImpl.setErrorState(t, true);
} finally {
context.unbind(oldCL);
}
}
}
// Has an error occurred during async processing that needs to be
// processed by the application's error page mechanism (or Tomcat's
// if the application doesn't define one)?
if (!request.isAsyncDispatching() && request.isAsync() && response.isErrorReportRequired()) {
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
if (request.isAsyncDispatching()) {
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
if (response.isError()) {
Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
asyncConImpl.setErrorState(t, true);
}
}
if (!request.isAsync()) {
request.finishRequest();
response.finishResponse();
}
// Check to see if the processor is in an error state. If it is,
// bail out now.
AtomicBoolean error = new AtomicBoolean(false);
res.action(ActionCode.IS_ERROR, error);
if (error.get()) {
if (request.isAsyncCompleting() || request.isAsyncDispatching()) {
// Connection will be forcibly closed which will prevent completion/dispatch happening at the usual
// point. Trigger post processing here.
res.action(ActionCode.ASYNC_POST_PROCESS, null);
}
success = false;
}
} catch (IOException e) {
success = false;
// Ignore
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
success = false;
log.error(sm.getString("coyoteAdapter.asyncDispatch"), t);
} finally {
if (!success) {
res.setStatus(500);
}
// Access logging
if (!success || !request.isAsync()) {
long time = 0;
if (req.getStartTimeNanos() != -1) {
time = System.nanoTime() - req.getStartTimeNanos();
}
Context context = request.getContext();
if (context != null) {
context.logAccess(request, response, time, false);
} else {
log(req, res, time);
}
}
req.getRequestProcessor().setWorkerThreadName(null);
req.clearRequestThread();
// Recycle the wrapper request and response
if (!success || !request.isAsync()) {
updateWrapperErrorCount(request, response);
request.recycle();
response.recycle();
}
}
return success;
}
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// Create objects
request = connector.createRequest(req);
response = connector.createResponse(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().setQueryStringCharset(connector.getURICharset());
}
if (connector.getXpoweredBy()) {
response.addHeader("X-Powered-By", POWERED_BY);
}
boolean async = false;
boolean postParseSuccess = false;
req.setRequestThread();
try {
// Parse and set Catalina and configuration specific
// request parameters
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
// check valves if we support async
request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
if (request.isAsync()) {
async = true;
ReadListener readListener = req.getReadListener();
if (readListener != null && request.isFinished()) {
// Possible the all data may have been read during service()
// method so this needs to be checked here
ClassLoader oldCL = null;
try {
oldCL = request.getContext().bind(null);
if (req.sendAllDataReadEvent()) {
req.getReadListener().onAllDataRead();
}
} finally {
request.getContext().unbind(oldCL);
}
}
Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
// If an async request was started, is not going to end once
// this container thread finishes and an error occurred, trigger
// the async error process
if (!request.isAsyncCompleting() && throwable != null) {
request.getAsyncContextInternal().setErrorState(throwable, true);
}
} else {
request.finishRequest();
response.finishResponse();
}
} catch (IOException e) {
// Ignore
} finally {
AtomicBoolean error = new AtomicBoolean(false);
res.action(ActionCode.IS_ERROR, error);
if (request.isAsyncCompleting() && error.get()) {
// Connection will be forcibly closed which will prevent
// completion happening at the usual point. Need to trigger
// call to onComplete() here.
res.action(ActionCode.ASYNC_POST_PROCESS, null);
async = false;
}
// Access log
if (!async && postParseSuccess) {
// Log only if processing was invoked.
// If postParseRequest() failed, it has already logged it.
Context context = request.getContext();
Host host = request.getHost();
// If the context is null, it is likely that the endpoint was
// shutdown, this connection closed and the request recycled in
// a different thread. That thread will have updated the access
// log so it is OK not to update the access log here in that
// case.
// The other possibility is that an error occurred early in
// processing and the request could not be mapped to a Context.
// Log via the host or engine in that case.
long time = System.nanoTime() - req.getStartTimeNanos();
if (context != null) {
context.logAccess(request, response, time, false);
} else if (response.isError()) {
if (host != null) {
host.logAccess(request, response, time, false);
} else {
connector.getService().getContainer().logAccess(request, response, time, false);
}
}
}
req.getRequestProcessor().setWorkerThreadName(null);
req.clearRequestThread();
// Recycle the wrapper request and response
if (!async) {
updateWrapperErrorCount(request, response);
request.recycle();
response.recycle();
}
}
}
private void updateWrapperErrorCount(Request request, Response response) {
if (response.isError()) {
Wrapper wrapper = request.getWrapper();
if (wrapper != null) {
wrapper.incrementErrorCount();
}
}
}
@Override
public boolean prepare(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws IOException, ServletException {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
return postParseRequest(req, request, res, response);
}
@Override
public void log(org.apache.coyote.Request req, org.apache.coyote.Response res, long time) {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// Create objects
request = connector.createRequest(req);
response = connector.createResponse(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().setQueryStringCharset(connector.getURICharset());
}
try {
// Log at the lowest level available. logAccess() will be
// automatically called on parent containers.
boolean logged = false;
Context context = request.mappingData.context;
Host host = request.mappingData.host;
if (context != null) {
logged = true;
context.logAccess(request, response, time, true);
} else if (host != null) {
logged = true;
host.logAccess(request, response, time, true);
}
if (!logged) {
connector.getService().getContainer().logAccess(request, response, time, true);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.warn(sm.getString("coyoteAdapter.accesslogFail"), t);
} finally {
updateWrapperErrorCount(request, response);
request.recycle();
response.recycle();
}
}
private static class RecycleRequiredException extends Exception {
private static final long serialVersionUID = 1L;
}
@Override
public void checkRecycled(org.apache.coyote.Request req, org.apache.coyote.Response res) {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
String messageKey = null;
if (request != null && request.getHost() != null) {
messageKey = "coyoteAdapter.checkRecycled.request";
} else if (response != null && response.getContentWritten() != 0) {
messageKey = "coyoteAdapter.checkRecycled.response";
}
if (messageKey != null) {
// Log this request, as it has probably skipped the access log.
// The log() method will take care of recycling.
log(req, res, 0L);
if (connector.getState().isAvailable()) {
if (log.isInfoEnabled()) {
log.info(sm.getString(messageKey), new RecycleRequiredException());
}
} else {
// There may be some aborted requests.
// When connector shuts down, the request and response will not
// be reused, so there is no issue to warn about here.
if (log.isDebugEnabled()) {
log.debug(sm.getString(messageKey), new RecycleRequiredException());
}
}
}
}
@Override
public String getDomain() {
return connector.getDomain();
}
// ------------------------------------------------------ Protected Methods
/**
* Perform the necessary processing after the HTTP headers have been parsed to enable the request/response pair to
* be passed to the start of the container pipeline for processing.
*
* @param req The coyote request object
* @param request The catalina request object
* @param res The coyote response object
* @param response The catalina response object
*
* @return <code>true</code> if the request should be passed on to the start of the container pipeline, otherwise
* <code>false</code>
*
* @throws IOException If there is insufficient space in a buffer while processing headers
* @throws ServletException If the supported methods of the target servlet cannot be determined
*/
protected boolean postParseRequest(org.apache.coyote.Request req, Request request, org.apache.coyote.Response res,
Response response) throws IOException, ServletException {
// If the processor has set the scheme (AJP does this, HTTP does this if
// SSL is enabled) use this to set the secure flag as well. If the
// processor hasn't set it, use the settings from the connector
if (req.scheme().isNull()) {
// Use connector scheme and secure configuration, (defaults to
// "http" and false respectively)
req.scheme().setString(connector.getScheme());
request.setSecure(connector.getSecure());
} else {
// Use processor specified scheme to determine secure state
request.setSecure(req.scheme().equals("https"));
}
// 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) {
req.setServerPort(proxyPort);
} else if (req.getServerPort() == -1) {
// Not explicitly set. Use default ports based on the scheme
if (req.scheme().equals("https")) {
req.setServerPort(443);
} else {
req.setServerPort(80);
}
}
if (proxyName != null) {
req.serverName().setString(proxyName);
}
MessageBytes undecodedURI = req.requestURI();
// Check for ping OPTIONS * request
if (undecodedURI.equals("*")) {
if (req.method().equals("OPTIONS")) {
StringBuilder allow = new StringBuilder();
allow.append("GET, HEAD, POST, PUT, DELETE, OPTIONS");
// Trace if allowed
if (connector.getAllowTrace()) {
allow.append(", TRACE");
}
res.setHeader("Allow", allow.toString());
// Access log entry as processing won't reach AccessLogValve
connector.getService().getContainer().logAccess(request, response, 0, true);
return false;
} else {
response.sendError(400, sm.getString("coyoteAdapter.invalidURI"));
}
}
MessageBytes decodedURI = req.decodedURI();
// Filter CONNECT method
if (req.method().equals("CONNECT")) {
response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, sm.getString("coyoteAdapter.connect"));
} else {
// No URI for CONNECT requests
if (undecodedURI.getType() == MessageBytes.T_BYTES) {
if (connector.getRejectSuspiciousURIs()) {
if (checkSuspiciousURIs(undecodedURI.getByteChunk())) {
response.sendError(400, sm.getString("coyoteAdapter.invalidURI"));
}
}
// Copy the raw URI to the decodedURI
decodedURI.duplicate(undecodedURI);
// Parse (and strip out) the path parameters
parsePathParameters(req, request);
// URI decoding
// %xx decoding of the URL
try {
req.getURLDecoder().convert(decodedURI.getByteChunk(),
connector.getEncodedSolidusHandlingInternal());
} catch (IOException ioe) {
response.sendError(400, sm.getString("coyoteAdapter.invalidURIWithMessage", ioe.getMessage()));
}
// Normalization
if (normalize(req.decodedURI(), connector.getAllowBackslash())) {
// Character decoding
convertURI(decodedURI, request);
// URIEncoding values are limited to US-ASCII supersets.
// Therefore it is not necessary to check that the URI remains
// normalized after character decoding
} else {
response.sendError(400, sm.getString("coyoteAdapter.invalidURI"));
}
} else {
/*
* The URI is chars or String, and has been sent using an in-memory protocol handler. The following
* assumptions are made: - req.requestURI() has been set to the 'original' non-decoded, non-normalized
* URI - req.decodedURI() has been set to the decoded, normalized form of req.requestURI() -
* 'suspicious' URI filtering - if required - has already been performed
*/
decodedURI.toChars();
// Remove all path parameters; any needed path parameter should be set
// using the request object rather than passing it in the URL
CharChunk uriCC = decodedURI.getCharChunk();
int semicolon = uriCC.indexOf(';');
if (semicolon > 0) {
decodedURI.setChars(uriCC.getBuffer(), uriCC.getStart(), semicolon);
}
}
}
// Request mapping.
MessageBytes serverName;
if (connector.getUseIPVHosts()) {
serverName = req.localName();
if (serverName.isNull()) {
// well, they did ask for it
res.action(ActionCode.REQ_LOCAL_NAME_ATTRIBUTE, null);
}
} else {
serverName = req.serverName();
}
// Version for the second mapping loop and
// Context that we expect to get for that version
String version = null;
Context versionContext = null;
boolean mapRequired = true;
if (response.isError()) {
// An error this early means the URI is invalid. Ensure invalid data
// is not passed to the mapper. Note we still want the mapper to
// find the correct host.
decodedURI.recycle();
}
while (mapRequired) {
// This will map the the latest version by default
connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData());
// If there is no context at this point, either this is a 404
// because no ROOT context has been deployed or the URI was invalid
// so no context could be mapped.
if (request.getContext() == null) {
// Allow processing to continue.
// If present, the rewrite Valve may rewrite this to a valid
// request.
// The StandardEngineValve will handle the case of a missing
// Host and the StandardHostValve the case of a missing Context.
// If present, the error reporting valve will provide a response
// body.
return true;
}
// Now we have the context, we can parse the session ID from the URL
// (if any). Need to do this before we redirect in case we need to
// include the session id in the redirect
String sessionID;
if (request.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)) {
// Get the session ID if there was one
sessionID = request.getPathParameter(SessionConfig.getSessionUriParamName(request.getContext()));
if (sessionID != null) {
request.setRequestedSessionId(sessionID);
request.setRequestedSessionURL(true);
}
}
// Look for session ID in cookies and SSL session
try {
parseSessionCookiesId(request);
} catch (IllegalArgumentException e) {
// Too many cookies
if (!response.isError()) {
response.setError();
response.sendError(400, e.getMessage());
}
return true;
}
parseSessionSslId(request);
sessionID = request.getRequestedSessionId();
mapRequired = false;
if (version != null && request.getContext() == versionContext) {
// We got the version that we asked for. That is it.
} else {
version = null;
versionContext = null;
Context[] contexts = request.getMappingData().contexts;
// Single contextVersion means no need to remap
// No session ID means no possibility of remap
if (contexts != null && sessionID != null) {
// Find the context associated with the session
for (int i = contexts.length; i > 0; i--) {
Context ctxt = contexts[i - 1];
if (ctxt.getManager().findSession(sessionID) != null) {
// We found a context. Is it the one that has
// already been mapped?
if (!ctxt.equals(request.getMappingData().context)) {
// Set version so second time through mapping
// the correct context is found
version = ctxt.getWebappVersion();
versionContext = ctxt;
// Reset mapping
request.getMappingData().recycle();
mapRequired = true;
// Recycle cookies and session info in case the
// correct context is configured with different
// settings
request.recycleSessionInfo();
request.recycleCookieInfo(true);
}
break;
}
}
}
}
if (!mapRequired && request.getContext().getPaused()) {
// Found a matching context but it is paused. Mapping data will
// be wrong since some Wrappers may not be registered at this
// point.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Should never happen
}
// Reset mapping
request.getMappingData().recycle();
mapRequired = true;
}
}
// Possible redirect
MessageBytes redirectPathMB = request.getMappingData().redirectPath;
if (!redirectPathMB.isNull()) {
String redirectPath = URLEncoder.DEFAULT.encode(redirectPathMB.toString(), StandardCharsets.UTF_8);
String query = request.getQueryString();
if (request.isRequestedSessionIdFromURL()) {
// This is not optimal, but as this is not very common, it
// shouldn't matter
redirectPath = redirectPath + ";" + SessionConfig.getSessionUriParamName(request.getContext()) + "=" +
request.getRequestedSessionId();
}
if (query != null) {
// This is not optimal, but as this is not very common, it
// shouldn't matter
redirectPath = redirectPath + "?" + query;
}
response.sendRedirect(redirectPath);
request.getContext().logAccess(request, response, 0, true);
return false;
}
// Filter TRACE method
if (!connector.getAllowTrace() && req.method().equals("TRACE")) {
Wrapper wrapper = request.getWrapper();
String header = null;
if (wrapper != null) {
String[] methods = wrapper.getServletMethods();
if (methods != null) {
for (String method : methods) {
if ("TRACE".equals(method)) {
continue;
}
if (header == null) {
header = method;
} else {
header += ", " + method;
}
}
}
}
if (header != null) {
res.addHeader("Allow", header);
}
response.sendError(405, sm.getString("coyoteAdapter.trace"));
// Safe to skip the remainder of this method.
return true;
}
doConnectorAuthenticationAuthorization(req, request);
return true;
}
private void doConnectorAuthenticationAuthorization(org.apache.coyote.Request req, Request request) {
// Set the remote principal
String username = req.getRemoteUser().toString();
if (username != null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("coyoteAdapter.authenticate", username));
}
if (req.getRemoteUserNeedsAuthorization()) {
Authenticator authenticator = request.getContext().getAuthenticator();
if (!(authenticator instanceof AuthenticatorBase)) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("coyoteAdapter.authorize", username));
}
// Custom authenticator that may not trigger authorization.
// Do the authorization here to make sure it is done.
request.setUserPrincipal(request.getContext().getRealm().authenticate(username));
}
// If the Authenticator is an instance of AuthenticatorBase then
// it will check req.getRemoteUserNeedsAuthorization() and
// trigger authorization as necessary. It will also cache the
// result preventing excessive calls to the Realm.
} else {
// The connector isn't configured for authorization. Create a
// user without any roles using the supplied user name.
request.setUserPrincipal(new CoyotePrincipal(username));
}
}
// Set the authorization type
String authType = req.getAuthType().toString();
if (authType != null) {
request.setAuthType(authType);
}
}
/**
* Extract the path parameters from the request. This assumes parameters are of the form
* /path;name=value;name2=value2/ etc. Currently only really interested in the session ID that will be in this form.
* Other parameters can safely be ignored.
*
* @param req The Coyote request object
* @param request The Servlet request object
*/
protected void parsePathParameters(org.apache.coyote.Request req, Request request) {
// Process in bytes (this is default format so this is normally a NO-OP
req.decodedURI().toBytes();
ByteChunk uriBC = req.decodedURI().getByteChunk();
// The first character must always be '/' so start search at position 1.
// If the first character is ';' the URI will be rejected at the
// normalization stage
int semicolon = uriBC.indexOf(';', 1);
// Performance optimisation. Return as soon as it is known there are no
// path parameters;
if (semicolon == -1) {
return;
}
// What encoding to use? Some platforms, eg z/os, use a default
// encoding that doesn't give the expected result so be explicit
Charset charset = connector.getURICharset();
if (log.isTraceEnabled()) {
log.trace(sm.getString("coyoteAdapter.debug", "uriBC", uriBC.toString()));
log.trace(sm.getString("coyoteAdapter.debug", "semicolon", String.valueOf(semicolon)));
log.trace(sm.getString("coyoteAdapter.debug", "enc", charset.name()));
}
while (semicolon > -1) {
// Parse path param, and extract it from the decoded request URI
int start = uriBC.getStart();
int end = uriBC.getEnd();
int pathParamStart = semicolon + 1;
int pathParamEnd =
ByteChunk.findBytes(uriBC.getBuffer(), start + pathParamStart, end, new byte[] { ';', '/' });
String pv = null;
if (pathParamEnd >= 0) {
if (charset != null) {
pv = new String(uriBC.getBuffer(), start + pathParamStart, pathParamEnd - pathParamStart, charset);
}
// Extract path param from decoded request URI
byte[] buf = uriBC.getBuffer();
for (int i = 0; i < end - start - pathParamEnd; i++) {
buf[start + semicolon + i] = buf[start + i + pathParamEnd];
}
uriBC.setBytes(buf, start, end - start - pathParamEnd + semicolon);
} else {
if (charset != null) {
pv = new String(uriBC.getBuffer(), start + pathParamStart, (end - start) - pathParamStart, charset);
}
uriBC.setEnd(start + semicolon);
}
if (log.isTraceEnabled()) {
log.trace(sm.getString("coyoteAdapter.debug", "pathParamStart", String.valueOf(pathParamStart)));
log.trace(sm.getString("coyoteAdapter.debug", "pathParamEnd", String.valueOf(pathParamEnd)));
log.trace(sm.getString("coyoteAdapter.debug", "pv", pv));
}
if (pv != null) {
int equals = pv.indexOf('=');
if (equals > -1) {
String name = pv.substring(0, equals);
String value = pv.substring(equals + 1);
request.addPathParameter(name, value);
if (log.isTraceEnabled()) {
log.trace(sm.getString("coyoteAdapter.debug", "equals", String.valueOf(equals)));
log.trace(sm.getString("coyoteAdapter.debug", "name", name));
log.trace(sm.getString("coyoteAdapter.debug", "value", value));
}
}
}
semicolon = uriBC.indexOf(';', semicolon);
}
}
/**
* Look for SSL session ID if required. Only look for SSL Session ID if it is the only tracking method enabled.
*
* @param request The Servlet request object
*/
protected void parseSessionSslId(Request request) {
if (request.getRequestedSessionId() == null &&
SSL_ONLY.equals(request.getServletContext().getEffectiveSessionTrackingModes()) &&
request.connector.secure) {
String sessionId = (String) request.getAttribute(SSLSupport.SESSION_ID_KEY);
if (sessionId != null) {
request.setRequestedSessionId(sessionId);
request.setRequestedSessionSSL(true);
}
}
}
/**
* Parse session id in Cookie.
*
* @param request The Servlet request object
*/
protected void parseSessionCookiesId(Request request) {
// If session tracking via cookies has been disabled for the current
// context, don't go looking for a session ID in a cookie as a cookie
// from a parent context with a session ID may be present which would
// overwrite the valid session ID encoded in the URL
Context context = request.getMappingData().context;
if (context != null &&
!context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE)) {
return;
}
// Parse session id from cookies
ServerCookies serverCookies = request.getServerCookies();
int count = serverCookies.getCookieCount();
if (count <= 0) {
return;
}
String sessionCookieName = SessionConfig.getSessionCookieName(context);
for (int i = 0; i < count; i++) {
ServerCookie scookie = serverCookies.getCookie(i);
if (scookie.getName().equals(sessionCookieName)) {
// Override anything requested in the URL
if (!request.isRequestedSessionIdFromCookie()) {
// Accept only the first session id cookie
convertMB(scookie.getValue());
request.setRequestedSessionId(scookie.getValue().toString());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
if (log.isTraceEnabled()) {
log.trace(" Requested cookie session id is " + request.getRequestedSessionId());
}
} else {
if (!request.isRequestedSessionIdValid()) {
// Replace the session id until one is valid
convertMB(scookie.getValue());
request.setRequestedSessionId(scookie.getValue().toString());
}
}
}
}
}
/**
* Character conversion of the URI.
*
* @param uri MessageBytes object containing the URI
* @param request The Servlet request object
*
* @throws IOException if a IO exception occurs sending an error to the client
*/
protected void convertURI(MessageBytes uri, Request request) throws IOException {
ByteChunk bc = uri.getByteChunk();
int length = bc.getLength();
CharChunk cc = uri.getCharChunk();
cc.allocate(length, -1);
Charset charset = connector.getURICharset();
B2CConverter conv = request.getURIConverter();
if (conv == null) {
conv = new B2CConverter(charset, false);
request.setURIConverter(conv);
} else {
conv.recycle();
}
try {
conv.convert(bc, cc, true);
uri.setChars(cc.getBuffer(), cc.getStart(), cc.getLength());
} catch (IOException ioe) {
// Should never happen as B2CConverter should replace
// problematic characters
request.getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
/**
* Character conversion of the a US-ASCII MessageBytes.
*
* @param mb The MessageBytes instance containing the bytes that should be converted to chars
*/
protected void convertMB(MessageBytes mb) {
// This is of course only meaningful for bytes
if (mb.getType() != MessageBytes.T_BYTES) {
return;
}
ByteChunk bc = mb.getByteChunk();
CharChunk cc = mb.getCharChunk();
int length = bc.getLength();
cc.allocate(length, -1);
// Default encoding: fast conversion
byte[] bbuf = bc.getBuffer();
char[] cbuf = cc.getBuffer();
int start = bc.getStart();
for (int i = 0; i < length; i++) {
cbuf[i] = (char) (bbuf[i + start] & 0xff);
}
mb.setChars(cbuf, 0, length);
}
/**
* This method normalizes "\", "//", "/./" and "/../".
*
* @param uriMB URI to be normalized
* @param allowBackslash <code>true</code> if backslash characters are allowed in URLs
*
* @return <code>false</code> if normalizing this URI would require going above the root, or if the URI contains a
* null byte, otherwise <code>true</code>
*/
public static boolean normalize(MessageBytes uriMB, boolean allowBackslash) {
ByteChunk uriBC = uriMB.getByteChunk();
final byte[] b = uriBC.getBytes();
final int start = uriBC.getStart();
int end = uriBC.getEnd();
boolean appendedSlash = false;
// An empty URL is not acceptable
if (start == end) {
return false;
}
int pos = 0;
int index = 0;
// The URL must start with '/' (or '\' that will be replaced soon)
if (b[start] != (byte) '/' && b[start] != (byte) '\\') {
return false;
}
// Replace '\' with '/'
// Check for null byte
for (pos = start; pos < end; pos++) {
if (b[pos] == (byte) '\\') {
if (allowBackslash) {
b[pos] = (byte) '/';
} else {
return false;
}
} else if (b[pos] == (byte) 0) {
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++;
appendedSlash = true;
}
}
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;
}
// If a slash was appended to help normalize "/." or "/.." then remove
// any trailing "/" from the result unless the result is "/".
if (appendedSlash && end > 1 && b[end - 1] == '/') {
uriBC.setEnd(end - 1);
}
return true;
}
/**
* Copy an array of bytes to a different position. Used during normalization.
*
* @param b The bytes that should be copied
* @param dest Destination offset
* @param src Source offset
* @param len Length
*/
protected static void copyBytes(byte[] b, int dest, int src, int len) {
System.arraycopy(b, src, b, dest, len);
}
/*
* Examine URI segment by segment for 'suspicious' URIs.
*/
private static boolean checkSuspiciousURIs(ByteChunk undecodedURI) {
byte[] bytes = undecodedURI.getBytes();
int start = undecodedURI.getStart();
int end = undecodedURI.getEnd();
int segmentStart = -1;
int segmentEnd = -1;
// Find first segment
segmentStart = undecodedURI.indexOf('/', 0);
if (segmentStart > -1) {
segmentEnd = undecodedURI.indexOf('/', segmentStart + 1);
}
while (segmentStart > -1) {
int pos = start + segmentStart + 1;
// Empty segment other than final segment with path parameters
if (segmentEnd > 0 && bytes[pos] == ';') {
return true;
}
// encoded dot-segments and/or dot-segments with path parameters
int dotCount = 0;
boolean encodedDot = false;
while (pos < end) {
if (bytes[pos] == '.') {
dotCount++;
pos++;
} else if (pos + 2 < end && bytes[pos] == '%' && bytes[pos + 1] == '2' &&
(bytes[pos + 2] == 'e' || bytes[pos + 2] == 'E')) {
encodedDot = true;
dotCount++;
pos += 3;
} else if (bytes[pos] == ';') {
if (dotCount > 0) {
return true;
}
break;
} else if (bytes[pos] == '/') {
break;
} else {
dotCount = 0;
break;
}
}
if (dotCount > 0 && encodedDot) {
return true;
}
// %nn encoded controls or '/'
pos = start + segmentStart + 1;
while (pos < end) {
if (pos + 2 < end && bytes[pos] == '%') {
byte b1 = bytes[pos + 1];
byte b2 = bytes[pos + 2];
pos += 3;
int decoded = (HexUtils.getDec(b1) << 4) + HexUtils.getDec(b2);
if (decoded < 20 || decoded == 0x7F || decoded == 0x2F) {
return true;
}
} else {
pos++;
}
}
// Move to next segment
if (segmentEnd == -1) {
segmentStart = -1;
} else {
segmentStart = segmentEnd;
if (segmentStart > -1) {
segmentEnd = undecodedURI.indexOf('/', segmentStart + 1);
}
}
}
return false;
}
}