/*
 *  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.coyote.ajp;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import jakarta.servlet.http.HttpServletResponse;

import org.apache.coyote.AbstractProcessor;
import org.apache.coyote.ActionCode;
import org.apache.coyote.Adapter;
import org.apache.coyote.ContinueResponseTiming;
import org.apache.coyote.ErrorState;
import org.apache.coyote.InputBuffer;
import org.apache.coyote.OutputBuffer;
import org.apache.coyote.RequestInfo;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
import org.apache.tomcat.util.net.ApplicationBufferHandler;
import org.apache.tomcat.util.net.SSLSupport;
import org.apache.tomcat.util.net.SocketWrapperBase;
import org.apache.tomcat.util.res.StringManager;

AJP Processor implementation.
/** * AJP Processor implementation. */
public class AjpProcessor extends AbstractProcessor { private static final Log log = LogFactory.getLog(AjpProcessor.class);
The string manager for this package.
/** * The string manager for this package. */
private static final StringManager sm = StringManager.getManager(AjpProcessor.class);
End message array.
/** * End message array. */
private static final byte[] endMessageArray; private static final byte[] endAndCloseMessageArray;
Flush message array.
/** * Flush message array. */
private static final byte[] flushMessageArray;
Pong message array.
/** * Pong message array. */
private static final byte[] pongMessageArray; private static final Map<String,String> jakartaAttributeMapping; private static final Set<String> iisTlsAttributes; static { // Allocate the end message array AjpMessage endMessage = new AjpMessage(16); endMessage.reset(); endMessage.appendByte(Constants.JK_AJP13_END_RESPONSE); endMessage.appendByte(1); endMessage.end(); endMessageArray = new byte[endMessage.getLen()]; System.arraycopy(endMessage.getBuffer(), 0, endMessageArray, 0, endMessage.getLen()); // Allocate the end and close message array AjpMessage endAndCloseMessage = new AjpMessage(16); endAndCloseMessage.reset(); endAndCloseMessage.appendByte(Constants.JK_AJP13_END_RESPONSE); endAndCloseMessage.appendByte(0); endAndCloseMessage.end(); endAndCloseMessageArray = new byte[endAndCloseMessage.getLen()]; System.arraycopy(endAndCloseMessage.getBuffer(), 0, endAndCloseMessageArray, 0, endAndCloseMessage.getLen()); // Allocate the flush message array AjpMessage flushMessage = new AjpMessage(16); flushMessage.reset(); flushMessage.appendByte(Constants.JK_AJP13_SEND_BODY_CHUNK); flushMessage.appendInt(0); flushMessage.appendByte(0); flushMessage.end(); flushMessageArray = new byte[flushMessage.getLen()]; System.arraycopy(flushMessage.getBuffer(), 0, flushMessageArray, 0, flushMessage.getLen()); // Allocate the pong message array AjpMessage pongMessage = new AjpMessage(16); pongMessage.reset(); pongMessage.appendByte(Constants.JK_AJP13_CPONG_REPLY); pongMessage.end(); pongMessageArray = new byte[pongMessage.getLen()]; System.arraycopy(pongMessage.getBuffer(), 0, pongMessageArray, 0, pongMessage.getLen()); // Build Map of Java Servlet to Jakarta Servlet attribute names Map<String,String> m = new HashMap<>(); m.put("jakarta.servlet.request.cipher_suite", "jakarta.servlet.request.cipher_suite"); m.put("jakarta.servlet.request.key_size", "jakarta.servlet.request.key_size"); m.put("jakarta.servlet.request.ssl_session", "jakarta.servlet.request.ssl_session"); m.put("jakarta.servlet.request.X509Certificate", "jakarta.servlet.request.X509Certificate"); m.put("javax.servlet.request.cipher_suite", "jakarta.servlet.request.cipher_suite"); m.put("javax.servlet.request.key_size", "jakarta.servlet.request.key_size"); m.put("javax.servlet.request.ssl_session", "jakarta.servlet.request.ssl_session"); m.put("javax.servlet.request.X509Certificate", "jakarta.servlet.request.X509Certificate"); jakartaAttributeMapping = Collections.unmodifiableMap(m); Set<String> s = new HashSet<>(); s.add("CERT_ISSUER"); s.add("CERT_SUBJECT"); s.add("CERT_COOKIE"); s.add("HTTPS_SERVER_SUBJECT"); s.add("CERT_FLAGS"); s.add("HTTPS_SECRETKEYSIZE"); s.add("CERT_SERIALNUMBER"); s.add("HTTPS_SERVER_ISSUER"); s.add("HTTPS_KEYSIZE"); iisTlsAttributes = Collections.unmodifiableSet(s); } // ----------------------------------------------------- Instance Variables private final AbstractAjpProtocol<?> protocol;
GetBody message array. Not static like the other message arrays since the message varies with packetSize and that can vary per connector.
/** * GetBody message array. Not static like the other message arrays since the * message varies with packetSize and that can vary per connector. */
private final byte[] getBodyMessageArray;
AJP packet size.
/** * AJP packet size. */
private final int outputMaxChunkSize;
Header message. Note that this header is merely the one used during the processing of the first message of a "request", so it might not be a request header. It will stay unchanged during the processing of the whole request.
/** * Header message. Note that this header is merely the one used during the * processing of the first message of a "request", so it might not be a * request header. It will stay unchanged during the processing of the whole * request. */
private final AjpMessage requestHeaderMessage;
Message used for response composition.
/** * Message used for response composition. */
private final AjpMessage responseMessage;
Location of next write of the response message (used with non-blocking writes when the message may not be written in a single write). A value of -1 indicates that no message has been written to the buffer.
/** * Location of next write of the response message (used with non-blocking * writes when the message may not be written in a single write). A value of * -1 indicates that no message has been written to the buffer. */
private int responseMsgPos = -1;
Body message.
/** * Body message. */
private final AjpMessage bodyMessage;
Body message.
/** * Body message. */
private final MessageBytes bodyBytes = MessageBytes.newInstance();
Temp message bytes used for processing.
/** * Temp message bytes used for processing. */
private final MessageBytes tmpMB = MessageBytes.newInstance();
Byte chunk for certs.
/** * Byte chunk for certs. */
private final MessageBytes certificates = MessageBytes.newInstance();
End of stream flag.
/** * End of stream flag. */
private boolean endOfStream = false;
Request body empty flag.
/** * Request body empty flag. */
private boolean empty = true;
First read.
/** * First read. */
private boolean first = true;
Indicates that a 'get body chunk' message has been sent but the body chunk has not yet been received.
/** * Indicates that a 'get body chunk' message has been sent but the body * chunk has not yet been received. */
private boolean waitingForBodyMessage = false;
Replay read.
/** * Replay read. */
private boolean replay = false;
Should any response body be swallowed and not sent to the client.
/** * Should any response body be swallowed and not sent to the client. */
private boolean swallowResponse = false;
Finished response.
/** * Finished response. */
private boolean responseFinished = false;
Bytes written to client for the current request.
/** * Bytes written to client for the current request. */
private long bytesWritten = 0; // ------------------------------------------------------------ Constructor public AjpProcessor(AbstractAjpProtocol<?> protocol, Adapter adapter) { super(adapter); this.protocol = protocol; int packetSize = protocol.getPacketSize(); // Calculate maximum chunk size as packetSize may have been changed from // the default (Constants.MAX_PACKET_SIZE) this.outputMaxChunkSize = packetSize - Constants.SEND_HEAD_LEN; request.setInputBuffer(new SocketInputBuffer()); requestHeaderMessage = new AjpMessage(packetSize); responseMessage = new AjpMessage(packetSize); bodyMessage = new AjpMessage(packetSize); // Set the getBody message buffer AjpMessage getBodyMessage = new AjpMessage(16); getBodyMessage.reset(); getBodyMessage.appendByte(Constants.JK_AJP13_GET_BODY_CHUNK); // Adjust read size if packetSize != default (Constants.MAX_PACKET_SIZE) getBodyMessage.appendInt(Constants.MAX_READ_SIZE + packetSize - Constants.MAX_PACKET_SIZE); getBodyMessage.end(); getBodyMessageArray = new byte[getBodyMessage.getLen()]; System.arraycopy(getBodyMessage.getBuffer(), 0, getBodyMessageArray, 0, getBodyMessage.getLen()); response.setOutputBuffer(new SocketOutputBuffer()); } // --------------------------------------------------------- Public Methods @Override protected boolean flushBufferedWrite() throws IOException { if (hasDataToWrite()) { socketWrapper.flush(false); if (hasDataToWrite()) { // There is data to write but go via Response to // maintain a consistent view of non-blocking state response.checkRegisterForWrite(); return true; } } return false; } @Override protected void dispatchNonBlockingRead() { if (available(true) > 0) { super.dispatchNonBlockingRead(); } } @Override protected SocketState dispatchEndRequest() { // Set keep alive timeout for next request socketWrapper.setReadTimeout(protocol.getKeepAliveTimeout()); recycle(); if (protocol.isPaused()) { return SocketState.CLOSED; } else { return SocketState.OPEN; } } @Override public SocketState service(SocketWrapperBase<?> socket) throws IOException { RequestInfo rp = request.getRequestProcessor(); rp.setStage(org.apache.coyote.Constants.STAGE_PARSE); // Setting up the socket this.socketWrapper = socket; boolean cping = false; // Expected to block on the first read as there should be at least one // AJP message to read. boolean firstRead = true; while (!getErrorState().isError() && !protocol.isPaused()) { // Parsing the request header try { // Get first message of the request if (!readMessage(requestHeaderMessage, firstRead)) { break; } firstRead = false; // Processing the request so make sure the connection rather // than keep-alive timeout is used socketWrapper.setReadTimeout(protocol.getConnectionTimeout()); // Check message type, process right away and break if // not regular request processing int type = requestHeaderMessage.getByte(); if (type == Constants.JK_AJP13_CPING_REQUEST) { if (protocol.isPaused()) { recycle(); break; } cping = true; try { socketWrapper.write(true, pongMessageArray, 0, pongMessageArray.length); socketWrapper.flush(true); } catch (IOException e) { if (getLog().isDebugEnabled()) { getLog().debug("Pong message failed", e); } setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); } recycle(); continue; } else if(type != Constants.JK_AJP13_FORWARD_REQUEST) { // Unexpected packet type. Unread body packets should have // been swallowed in finish(). if (getLog().isDebugEnabled()) { getLog().debug("Unexpected message: " + type); } setErrorState(ErrorState.CLOSE_CONNECTION_NOW, null); break; } request.setStartTimeNanos(System.nanoTime()); } catch (IOException e) { setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); break; } catch (Throwable t) { ExceptionUtils.handleThrowable(t); getLog().debug(sm.getString("ajpprocessor.header.error"), t); // 400 - Bad Request response.setStatus(400); setErrorState(ErrorState.CLOSE_CLEAN, t); } if (getErrorState().isIoAllowed()) { // Setting up filters, and parse some request headers rp.setStage(org.apache.coyote.Constants.STAGE_PREPARE); try { prepareRequest(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); getLog().debug(sm.getString("ajpprocessor.request.prepare"), t); // 500 - Internal Server Error response.setStatus(500); setErrorState(ErrorState.CLOSE_CLEAN, t); } } if (getErrorState().isIoAllowed() && !cping && protocol.isPaused()) { // 503 - Service unavailable response.setStatus(503); setErrorState(ErrorState.CLOSE_CLEAN, null); } cping = false; // Process the request in the adapter if (getErrorState().isIoAllowed()) { try { rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE); getAdapter().service(request, response); } catch (InterruptedIOException e) { setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); getLog().error(sm.getString("ajpprocessor.request.process"), t); // 500 - Internal Server Error response.setStatus(500); setErrorState(ErrorState.CLOSE_CLEAN, t); getAdapter().log(request, response, 0); } } if (isAsync() && !getErrorState().isError()) { break; } // Finish the response if not done yet if (!responseFinished && getErrorState().isIoAllowed()) { try { action(ActionCode.COMMIT, null); finishResponse(); } catch (IOException ioe){ setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); setErrorState(ErrorState.CLOSE_NOW, t); } } // If there was an error, make sure the request is counted as // and error, and update the statistics counter if (getErrorState().isError()) { response.setStatus(500); } request.updateCounters(); rp.setStage(org.apache.coyote.Constants.STAGE_KEEPALIVE); // Set keep alive timeout for next request socketWrapper.setReadTimeout(protocol.getKeepAliveTimeout()); recycle(); } rp.setStage(org.apache.coyote.Constants.STAGE_ENDED); if (getErrorState().isError() || protocol.isPaused()) { return SocketState.CLOSED; } else { if (isAsync()) { return SocketState.LONG; } else { return SocketState.OPEN; } } } @Override public void recycle() { getAdapter().checkRecycled(request, response); super.recycle(); request.recycle(); response.recycle(); first = true; endOfStream = false; waitingForBodyMessage = false; empty = true; replay = false; responseFinished = false; certificates.recycle(); swallowResponse = false; bytesWritten = 0; } @Override public void pause() { // NOOP for AJP } // ------------------------------------------------------ Protected Methods // Methods used by SocketInputBuffer
Read an AJP body message. Used to read both the 'special' packet in ajp13 and to receive the data after we send a GET_BODY packet.
Params:
  • block – If there is no data available to read when this method is called, should this call block until data becomes available?
Returns:true if at least one body byte was read, otherwise false
/** * Read an AJP body message. Used to read both the 'special' packet in ajp13 * and to receive the data after we send a GET_BODY packet. * * @param block If there is no data available to read when this method is * called, should this call block until data becomes available? * * @return <code>true</code> if at least one body byte was read, otherwise * <code>false</code> */
private boolean receive(boolean block) throws IOException { bodyMessage.reset(); if (!readMessage(bodyMessage, block)) { return false; } waitingForBodyMessage = false; // No data received. if (bodyMessage.getLen() == 0) { // just the header return false; } int blen = bodyMessage.peekInt(); if (blen == 0) { return false; } bodyMessage.getBodyBytes(bodyBytes); empty = false; return true; }
Read an AJP message.
Params:
  • message – The message to populate
  • block – If there is no data available to read when this method is called, should this call block until data becomes available?
Throws:
  • IOException – any other failure, including incomplete reads
Returns:true if the message has been read, false if no data was read
/** * Read an AJP message. * * @param message The message to populate * @param block If there is no data available to read when this method is * called, should this call block until data becomes available? * @return true if the message has been read, false if no data was read * * @throws IOException any other failure, including incomplete reads */
private boolean readMessage(AjpMessage message, boolean block) throws IOException { byte[] buf = message.getBuffer(); if (!read(buf, 0, Constants.H_SIZE, block)) { return false; } int messageLength = message.processHeader(true); if (messageLength < 0) { // Invalid AJP header signature throw new IOException(sm.getString("ajpmessage.invalidLength", Integer.valueOf(messageLength))); } else if (messageLength == 0) { // Zero length message. return true; } else { if (messageLength > message.getBuffer().length) { // Message too long for the buffer // Need to trigger a 400 response String msg = sm.getString("ajpprocessor.header.tooLong", Integer.valueOf(messageLength), Integer.valueOf(buf.length)); log.error(msg); throw new IllegalArgumentException(msg); } read(buf, Constants.H_SIZE, messageLength, true); return true; } }
Get more request body data from the web server and store it in the internal buffer.
Params:
  • block – true if this is blocking IO
Throws:
Returns:true if there is more data, false if not.
/** * Get more request body data from the web server and store it in the * internal buffer. * @param block <code>true</code> if this is blocking IO * @return <code>true</code> if there is more data, * <code>false</code> if not. * @throws IOException An IO error occurred */
protected boolean refillReadBuffer(boolean block) throws IOException { // When using replay (e.g. after FORM auth) all the data to read has // been buffered so there is no opportunity to refill the buffer. if (replay) { endOfStream = true; // we've read everything there is } if (endOfStream) { return false; } if (first) { first = false; long contentLength = request.getContentLengthLong(); // - When content length > 0, AJP sends the first body message // automatically. // - When content length == 0, AJP does not send a body message. // - When content length is unknown, AJP does not send the first // body message automatically. if (contentLength > 0) { waitingForBodyMessage = true; } else if (contentLength == 0) { endOfStream = true; return false; } } // Request more data immediately if (!waitingForBodyMessage) { socketWrapper.write(true, getBodyMessageArray, 0, getBodyMessageArray.length); socketWrapper.flush(true); waitingForBodyMessage = true; } boolean moreData = receive(block); if (!moreData && !waitingForBodyMessage) { endOfStream = true; } return moreData; }
After reading the request headers, we have to setup the request filters.
/** * After reading the request headers, we have to setup the request filters. */
private void prepareRequest() { // Translate the HTTP method code to a String. byte methodCode = requestHeaderMessage.getByte(); if (methodCode != Constants.SC_M_JK_STORED) { String methodName = Constants.getMethodForCode(methodCode - 1); request.method().setString(methodName); } requestHeaderMessage.getBytes(request.protocol()); requestHeaderMessage.getBytes(request.requestURI()); requestHeaderMessage.getBytes(request.remoteAddr()); requestHeaderMessage.getBytes(request.remoteHost()); requestHeaderMessage.getBytes(request.localName()); request.setLocalPort(requestHeaderMessage.getInt()); boolean isSSL = requestHeaderMessage.getByte() != 0; if (isSSL) { request.scheme().setString("https"); } // Decode headers MimeHeaders headers = request.getMimeHeaders(); // Set this every time in case limit has been changed via JMX headers.setLimit(protocol.getMaxHeaderCount()); boolean contentLengthSet = false; int hCount = requestHeaderMessage.getInt(); for(int i = 0 ; i < hCount ; i++) { String hName = null; // Header names are encoded as either an integer code starting // with 0xA0, or as a normal string (in which case the first // two bytes are the length). int isc = requestHeaderMessage.peekInt(); int hId = isc & 0xFF; MessageBytes vMB = null; isc &= 0xFF00; if(0xA000 == isc) { requestHeaderMessage.getInt(); // To advance the read position hName = Constants.getHeaderForCode(hId - 1); vMB = headers.addValue(hName); } else { // reset hId -- if the header currently being read // happens to be 7 or 8 bytes long, the code below // will think it's the content-type header or the // content-length header - SC_REQ_CONTENT_TYPE=7, // SC_REQ_CONTENT_LENGTH=8 - leading to unexpected // behaviour. see bug 5861 for more information. hId = -1; requestHeaderMessage.getBytes(tmpMB); ByteChunk bc = tmpMB.getByteChunk(); vMB = headers.addValue(bc.getBuffer(), bc.getStart(), bc.getLength()); } requestHeaderMessage.getBytes(vMB); if (hId == Constants.SC_REQ_CONTENT_LENGTH || (hId == -1 && tmpMB.equalsIgnoreCase("Content-Length"))) { long cl = vMB.getLong(); if (contentLengthSet) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); setErrorState(ErrorState.CLOSE_CLEAN, null); } else { contentLengthSet = true; // Set the content-length header for the request request.setContentLength(cl); } } else if (hId == Constants.SC_REQ_CONTENT_TYPE || (hId == -1 && tmpMB.equalsIgnoreCase("Content-Type"))) { // just read the content-type header, so set it ByteChunk bchunk = vMB.getByteChunk(); request.contentType().setBytes(bchunk.getBytes(), bchunk.getOffset(), bchunk.getLength()); } } // Decode extra attributes String secret = protocol.getSecret(); boolean secretPresentInRequest = false; byte attributeCode; while ((attributeCode = requestHeaderMessage.getByte()) != Constants.SC_A_ARE_DONE) { switch (attributeCode) { case Constants.SC_A_REQ_ATTRIBUTE : requestHeaderMessage.getBytes(tmpMB); String n = tmpMB.toString(); requestHeaderMessage.getBytes(tmpMB); String v = tmpMB.toString(); /* * AJP13 misses to forward the local IP address and the * remote port. Allow the AJP connector to add this info via * private request attributes. * We will accept the forwarded data and remove it from the * public list of request attributes. */ if(n.equals(Constants.SC_A_REQ_LOCAL_ADDR)) { request.localAddr().setString(v); } else if(n.equals(Constants.SC_A_REQ_REMOTE_PORT)) { try { request.setRemotePort(Integer.parseInt(v)); } catch (NumberFormatException nfe) { // Ignore invalid value } } else if(n.equals(Constants.SC_A_SSL_PROTOCOL)) { request.setAttribute(SSLSupport.PROTOCOL_VERSION_KEY, v); } else if (n.equals("JK_LB_ACTIVATION")) { request.setAttribute(n, v); } else if (jakartaAttributeMapping.containsKey(n)) { // AJP uses the Java Servlet attribute names. // Need to convert these to Jakarta Servlet. request.setAttribute(jakartaAttributeMapping.get(n), v); } else if (iisTlsAttributes.contains(n)) { // Allow IIS TLS attributes request.setAttribute(n, v); } else { // All 'known' attributes will be processed by the previous // blocks. Any remaining attribute is an 'arbitrary' one. Pattern pattern = protocol.getAllowedRequestAttributesPatternInternal(); if (pattern != null && pattern.matcher(n).matches()) { request.setAttribute(n, v); } else { log.warn(sm.getString("ajpprocessor.unknownAttribute", n)); response.setStatus(403); setErrorState(ErrorState.CLOSE_CLEAN, null); } } break; case Constants.SC_A_CONTEXT : requestHeaderMessage.getBytes(tmpMB); // nothing break; case Constants.SC_A_SERVLET_PATH : requestHeaderMessage.getBytes(tmpMB); // nothing break; case Constants.SC_A_REMOTE_USER : boolean tomcatAuthorization = protocol.getTomcatAuthorization(); if (tomcatAuthorization || !protocol.getTomcatAuthentication()) { // Implies tomcatAuthentication == false requestHeaderMessage.getBytes(request.getRemoteUser()); request.setRemoteUserNeedsAuthorization(tomcatAuthorization); } else { // Ignore user information from reverse proxy requestHeaderMessage.getBytes(tmpMB); } break; case Constants.SC_A_AUTH_TYPE : if (protocol.getTomcatAuthentication()) { // ignore server requestHeaderMessage.getBytes(tmpMB); } else { requestHeaderMessage.getBytes(request.getAuthType()); } break; case Constants.SC_A_QUERY_STRING : requestHeaderMessage.getBytes(request.queryString()); break; case Constants.SC_A_JVM_ROUTE : requestHeaderMessage.getBytes(tmpMB); // nothing break; case Constants.SC_A_SSL_CERT : // SSL certificate extraction is lazy, moved to JkCoyoteHandler requestHeaderMessage.getBytes(certificates); break; case Constants.SC_A_SSL_CIPHER : requestHeaderMessage.getBytes(tmpMB); request.setAttribute(SSLSupport.CIPHER_SUITE_KEY, tmpMB.toString()); break; case Constants.SC_A_SSL_SESSION : requestHeaderMessage.getBytes(tmpMB); request.setAttribute(SSLSupport.SESSION_ID_KEY, tmpMB.toString()); break; case Constants.SC_A_SSL_KEY_SIZE : request.setAttribute(SSLSupport.KEY_SIZE_KEY, Integer.valueOf(requestHeaderMessage.getInt())); break; case Constants.SC_A_STORED_METHOD: requestHeaderMessage.getBytes(request.method()); break; case Constants.SC_A_SECRET: requestHeaderMessage.getBytes(tmpMB); if (secret != null && secret.length() > 0) { secretPresentInRequest = true; if (!tmpMB.equals(secret)) { response.setStatus(403); setErrorState(ErrorState.CLOSE_CLEAN, null); } } break; default: // Ignore unknown attribute for backward compatibility break; } } // Check if secret was submitted if required if (secret != null && secret.length() > 0 && !secretPresentInRequest) { response.setStatus(403); setErrorState(ErrorState.CLOSE_CLEAN, null); } // Check for a full URI (including protocol://host:port/) ByteChunk uriBC = request.requestURI().getByteChunk(); if (uriBC.startsWithIgnoreCase("http", 0)) { int pos = uriBC.indexOf("://", 0, 3, 4); int uriBCStart = uriBC.getStart(); int slashPos = -1; if (pos != -1) { byte[] uriB = uriBC.getBytes(); slashPos = uriBC.indexOf('/', pos + 3); if (slashPos == -1) { slashPos = uriBC.getLength(); // Set URI as "/" request.requestURI().setBytes (uriB, uriBCStart + pos + 1, 1); } else { request.requestURI().setBytes (uriB, uriBCStart + slashPos, uriBC.getLength() - slashPos); } MessageBytes hostMB = headers.setValue("host"); hostMB.setBytes(uriB, uriBCStart + pos + 3, slashPos - pos - 3); } } MessageBytes valueMB = request.getMimeHeaders().getValue("host"); parseHost(valueMB); if (!getErrorState().isIoAllowed()) { getAdapter().log(request, response, 0); } }
{@inheritDoc}

This implementation populates the server name from the local name provided by the AJP message.

/** * {@inheritDoc} * <p> * This implementation populates the server name from the local name * provided by the AJP message. */
@Override protected void populateHost() { try { request.serverName().duplicate(request.localName()); } catch (IOException e) { response.setStatus(400); setErrorState(ErrorState.CLOSE_CLEAN, e); } }
{@inheritDoc}

This implementation populates the server port from the local port provided by the AJP message.

/** * {@inheritDoc} * <p> * This implementation populates the server port from the local port * provided by the AJP message. */
@Override protected void populatePort() { // No host information (HTTP/1.0) request.setServerPort(request.getLocalPort()); }
When committing the response, we have to validate the set of headers, as well as setup the response filters.
/** * When committing the response, we have to validate the set of headers, as * well as setup the response filters. */
@Override protected final void prepareResponse() throws IOException { response.setCommitted(true); tmpMB.recycle(); responseMsgPos = -1; responseMessage.reset(); responseMessage.appendByte(Constants.JK_AJP13_SEND_HEADERS); // Responses with certain status codes are not permitted to include a // response body. int statusCode = response.getStatus(); if (statusCode < 200 || statusCode == 204 || statusCode == 205 || statusCode == 304) { // No entity body swallowResponse = true; } // Responses to HEAD requests are not permitted to include a response // body. MessageBytes methodMB = request.method(); if (methodMB.equals("HEAD")) { // No entity body swallowResponse = true; } // HTTP header contents responseMessage.appendInt(statusCode); // Reason phrase is optional but mod_jk + httpd 2.x fails with a null // reason phrase - bug 45026 tmpMB.setString(Integer.toString(response.getStatus())); responseMessage.appendBytes(tmpMB); // Special headers MimeHeaders headers = response.getMimeHeaders(); String contentType = response.getContentType(); if (contentType != null) { headers.setValue("Content-Type").setString(contentType); } String contentLanguage = response.getContentLanguage(); if (contentLanguage != null) { headers.setValue("Content-Language").setString(contentLanguage); } long contentLength = response.getContentLengthLong(); if (contentLength >= 0) { headers.setValue("Content-Length").setLong(contentLength); } // Other headers int numHeaders = headers.size(); responseMessage.appendInt(numHeaders); for (int i = 0; i < numHeaders; i++) { MessageBytes hN = headers.getName(i); int hC = Constants.getResponseAjpIndex(hN.toString()); if (hC > 0) { responseMessage.appendInt(hC); } else { responseMessage.appendBytes(hN); } MessageBytes hV=headers.getValue(i); responseMessage.appendBytes(hV); } // Write to buffer responseMessage.end(); socketWrapper.write(true, responseMessage.getBuffer(), 0, responseMessage.getLen()); socketWrapper.flush(true); }
Callback to write data from the buffer.
/** * Callback to write data from the buffer. */
@Override protected final void flush() throws IOException { // Calling code should ensure that there is no data in the buffers for // non-blocking writes. // TODO Validate the assertion above if (!responseFinished) { if (protocol.getAjpFlush()) { // Send the flush message socketWrapper.write(true, flushMessageArray, 0, flushMessageArray.length); } socketWrapper.flush(true); } }
Finish AJP response.
/** * Finish AJP response. */
@Override protected final void finishResponse() throws IOException { if (responseFinished) return; responseFinished = true; // Swallow the unread body packet if present if (waitingForBodyMessage || first && request.getContentLengthLong() > 0) { refillReadBuffer(true); } // Add the end message if (getErrorState().isError()) { socketWrapper.write(true, endAndCloseMessageArray, 0, endAndCloseMessageArray.length); } else { socketWrapper.write(true, endMessageArray, 0, endMessageArray.length); } socketWrapper.flush(true); } @Override protected final void ack(ContinueResponseTiming continueResponseTiming) { // NO-OP for AJP } @Override protected final int available(boolean doRead) { if (endOfStream) { return 0; } if (empty && doRead) { try { refillReadBuffer(false); } catch (IOException timeout) { // Not ideal. This will indicate that data is available // which should trigger a read which in turn will trigger // another IOException and that one can be thrown. return 1; } } if (empty) { return 0; } else { return request.getInputBuffer().available(); } } @Override protected final void setRequestBody(ByteChunk body) { int length = body.getLength(); bodyBytes.setBytes(body.getBytes(), body.getStart(), length); request.setContentLength(length); first = false; empty = false; replay = true; endOfStream = false; } @Override protected final void setSwallowResponse() { swallowResponse = true; } @Override protected final void disableSwallowRequest() { /* NO-OP * With AJP, Tomcat controls when the client sends request body data. At * most there will be a single packet to read and that will be handled * in finishResponse(). */ } @Override protected final boolean getPopulateRequestAttributesFromSocket() { // NO-OPs the attribute requests since they are pre-populated when // parsing the first AJP message. return false; } @Override protected final void populateRequestAttributeRemoteHost() { // Get remote host name using a DNS resolution if (request.remoteHost().isNull()) { try { request.remoteHost().setString(InetAddress.getByName (request.remoteAddr().toString()).getHostName()); } catch (IOException iex) { // Ignore } } } @Override protected final void populateSslRequestAttributes() { if (!certificates.isNull()) { ByteChunk certData = certificates.getByteChunk(); X509Certificate jsseCerts[] = null; ByteArrayInputStream bais = new ByteArrayInputStream(certData.getBytes(), certData.getStart(), certData.getLength()); // Fill the elements. try { CertificateFactory cf; String clientCertProvider = protocol.getClientCertProvider(); if (clientCertProvider == null) { cf = CertificateFactory.getInstance("X.509"); } else { cf = CertificateFactory.getInstance("X.509", clientCertProvider); } while(bais.available() > 0) { X509Certificate cert = (X509Certificate) cf.generateCertificate(bais); if(jsseCerts == null) { jsseCerts = new X509Certificate[1]; jsseCerts[0] = cert; } else { X509Certificate [] temp = new X509Certificate[jsseCerts.length+1]; System.arraycopy(jsseCerts,0,temp,0,jsseCerts.length); temp[jsseCerts.length] = cert; jsseCerts = temp; } } } catch (java.security.cert.CertificateException e) { getLog().error(sm.getString("ajpprocessor.certs.fail"), e); return; } catch (NoSuchProviderException e) { getLog().error(sm.getString("ajpprocessor.certs.fail"), e); return; } request.setAttribute(SSLSupport.CERTIFICATE_KEY, jsseCerts); } } @Override protected final boolean isRequestBodyFullyRead() { return endOfStream; } @Override protected final void registerReadInterest() { socketWrapper.registerReadInterest(); } @Override protected final boolean isReadyForWrite() { return responseMsgPos == -1 && socketWrapper.isReadyForWrite(); } @Override protected boolean isTrailerFieldsReady() { // AJP does not support trailers so return true so app can request the // trailers and find out that there are none. return true; }
Read at least the specified amount of bytes, and place them in the input buffer. Note that if any data is available to read then this method will always block until at least the specified number of bytes have been read.
Params:
  • buf – Buffer to read data into
  • pos – Start position
  • n – The minimum number of bytes to read
  • block – If there is no data available to read when this method is called, should this call block until data becomes available?
Throws:
Returns: true if the requested number of bytes were read else false
/** * Read at least the specified amount of bytes, and place them * in the input buffer. Note that if any data is available to read then this * method will always block until at least the specified number of bytes * have been read. * * @param buf Buffer to read data into * @param pos Start position * @param n The minimum number of bytes to read * @param block If there is no data available to read when this method is * called, should this call block until data becomes available? * @return <code>true</code> if the requested number of bytes were read * else <code>false</code> * @throws IOException */
private boolean read(byte[] buf, int pos, int n, boolean block) throws IOException { int read = socketWrapper.read(block, buf, pos, n); if (read > 0 && read < n) { int left = n - read; int start = pos + read; while (left > 0) { read = socketWrapper.read(true, buf, start, left); if (read == -1) { throw new EOFException(); } left = left - read; start = start + read; } } else if (read == -1) { throw new EOFException(); } return read > 0; } private void writeData(ByteBuffer chunk) throws IOException { boolean blocking = (response.getWriteListener() == null); int len = chunk.remaining(); int off = 0; // Write this chunk while (len > 0) { int thisTime = Math.min(len, outputMaxChunkSize); responseMessage.reset(); responseMessage.appendByte(Constants.JK_AJP13_SEND_BODY_CHUNK); chunk.limit(chunk.position() + thisTime); responseMessage.appendBytes(chunk); responseMessage.end(); socketWrapper.write(blocking, responseMessage.getBuffer(), 0, responseMessage.getLen()); socketWrapper.flush(blocking); len -= thisTime; off += thisTime; } bytesWritten += off; } private boolean hasDataToWrite() { return responseMsgPos != -1 || socketWrapper.hasDataToWrite(); } @Override protected Log getLog() { return log; } // ------------------------------------- InputStreamInputBuffer Inner Class
This class is an input buffer which will read its data from an input stream.
/** * This class is an input buffer which will read its data from an input * stream. */
protected class SocketInputBuffer implements InputBuffer { @Override public int doRead(ApplicationBufferHandler handler) throws IOException { if (endOfStream) { return -1; } if (empty) { if (!refillReadBuffer(true)) { return -1; } } ByteChunk bc = bodyBytes.getByteChunk(); handler.setByteBuffer(ByteBuffer.wrap(bc.getBuffer(), bc.getStart(), bc.getLength())); empty = true; return handler.getByteBuffer().remaining(); } @Override public int available() { if (empty) { return 0; } else { return bodyBytes.getByteChunk().getLength(); } } } // ----------------------------------- OutputStreamOutputBuffer Inner Class
This class is an output buffer which will write data to an output stream.
/** * This class is an output buffer which will write data to an output * stream. */
protected class SocketOutputBuffer implements OutputBuffer { @Override public int doWrite(ByteBuffer chunk) throws IOException { if (!response.isCommitted()) { // Validate and write response headers try { prepareResponse(); } catch (IOException e) { setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); } } int len = 0; if (!swallowResponse) { try { len = chunk.remaining(); writeData(chunk); len -= chunk.remaining(); } catch (IOException ioe) { setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); // Re-throw throw ioe; } } return len; } @Override public long getBytesWritten() { return bytesWritten; } } }