/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.server.protocol.ajp;
import io.undertow.UndertowLogger;
import io.undertow.UndertowOptions;
import io.undertow.conduits.ConduitListener;
import io.undertow.conduits.EmptyStreamSourceConduit;
import io.undertow.conduits.ReadDataStreamSourceConduit;
import io.undertow.server.AbstractServerConnection;
import io.undertow.server.ConnectorStatisticsImpl;
import io.undertow.server.Connectors;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.protocol.ParseTimeoutUpdater;
import io.undertow.util.HeaderMap;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import org.xnio.ChannelListener;
import io.undertow.connector.PooledByteBuffer;
import io.undertow.util.StatusCodes;
import io.undertow.util.BadRequestException;
import org.xnio.StreamConnection;
import org.xnio.channels.StreamSinkChannel;
import org.xnio.channels.StreamSourceChannel;
import org.xnio.conduits.ConduitStreamSinkChannel;
import org.xnio.conduits.ConduitStreamSourceChannel;
import org.xnio.conduits.StreamSourceConduit;
import org.xnio.conduits.WriteReadyHandler;
import java.io.IOException;
import java.nio.ByteBuffer;
import static org.xnio.IoUtils.safeClose;
/**
* @author Stuart Douglas
*/
final class AjpReadListener implements ChannelListener<StreamSourceChannel> {
private static final byte[] CPONG = {'A', 'B', 0, 1, 9}; //CPONG response data
private final AjpServerConnection connection;
private final String scheme;
private final boolean recordRequestStartTime;
private AjpRequestParseState state = new AjpRequestParseState();
private HttpServerExchange httpServerExchange;
private volatile int read = 0;
private final int maxRequestSize;
private final long maxEntitySize;
private final AjpRequestParser parser;
private final ConnectorStatisticsImpl connectorStatistics;
private WriteReadyHandler.ChannelListenerHandler<ConduitStreamSinkChannel> writeReadyHandler;
private ParseTimeoutUpdater parseTimeoutUpdater;
AjpReadListener(final AjpServerConnection connection, final String scheme, AjpRequestParser parser, ConnectorStatisticsImpl connectorStatistics) {
this.connection = connection;
this.scheme = scheme;
this.parser = parser;
this.connectorStatistics = connectorStatistics;
this.maxRequestSize = connection.getUndertowOptions().get(UndertowOptions.MAX_HEADER_SIZE, UndertowOptions.DEFAULT_MAX_HEADER_SIZE);
this.maxEntitySize = connection.getUndertowOptions().get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE);
this.writeReadyHandler = new WriteReadyHandler.ChannelListenerHandler<>(connection.getChannel().getSinkChannel());
this.recordRequestStartTime = connection.getUndertowOptions().get(UndertowOptions.RECORD_REQUEST_START_TIME, false);
int requestParseTimeout = connection.getUndertowOptions().get(UndertowOptions.REQUEST_PARSE_TIMEOUT, -1);
int requestIdleTimeout = connection.getUndertowOptions().get(UndertowOptions.NO_REQUEST_TIMEOUT, -1);
if(requestIdleTimeout < 0 && requestParseTimeout < 0) {
this.parseTimeoutUpdater = null;
} else {
this.parseTimeoutUpdater = new ParseTimeoutUpdater(connection, requestParseTimeout, requestIdleTimeout);
connection.addCloseListener(parseTimeoutUpdater);
}
}
public void startRequest() {
connection.resetChannel();
state = new AjpRequestParseState();
read = 0;
if(parseTimeoutUpdater != null) {
parseTimeoutUpdater.connectionIdle();
}
connection.setCurrentExchange(null);
}
public void handleEvent(final StreamSourceChannel channel) {
if(connection.getOriginalSinkConduit().isWriteShutdown() || connection.getOriginalSourceConduit().isReadShutdown()) {
safeClose(connection);
channel.suspendReads();
return;
}
PooledByteBuffer existing = connection.getExtraBytes();
final PooledByteBuffer pooled = existing == null ? connection.getByteBufferPool().allocate() : existing;
final ByteBuffer buffer = pooled.getBuffer();
boolean free = true;
boolean bytesRead = false;
try {
int res;
do {
if (existing == null) {
buffer.clear();
try {
res = channel.read(buffer);
} catch (IOException e) {
UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
safeClose(connection);
return;
}
} else {
res = buffer.remaining();
}
if (res == 0) {
if(bytesRead && parseTimeoutUpdater != null) {
parseTimeoutUpdater.failedParse();
}
if (!channel.isReadResumed()) {
channel.getReadSetter().set(this);
channel.resumeReads();
}
return;
}
if (res == -1) {
try {
channel.shutdownReads();
final StreamSinkChannel responseChannel = connection.getChannel().getSinkChannel();
responseChannel.shutdownWrites();
safeClose(connection);
} catch (IOException e) {
UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
// fuck it, it's all ruined
safeClose(connection);
return;
}
return;
}
bytesRead = true;
//TODO: we need to handle parse errors
if (existing != null) {
existing = null;
connection.setExtraBytes(null);
} else {
buffer.flip();
}
int begin = buffer.remaining();
if(httpServerExchange == null) {
httpServerExchange = new HttpServerExchange(connection, maxEntitySize);
}
parser.parse(buffer, state, httpServerExchange);
read += begin - buffer.remaining();
if (buffer.hasRemaining()) {
free = false;
connection.setExtraBytes(pooled);
}
if (read > maxRequestSize) {
UndertowLogger.REQUEST_LOGGER.requestHeaderWasTooLarge(connection.getPeerAddress(), maxRequestSize);
safeClose(connection);
return;
}
} while (!state.isComplete());
if(parseTimeoutUpdater != null) {
parseTimeoutUpdater.requestStarted();
}
if (state.prefix != AjpRequestParser.FORWARD_REQUEST) {
if (state.prefix == AjpRequestParser.CPING) {
UndertowLogger.REQUEST_LOGGER.debug("Received CPING, sending CPONG");
handleCPing();
} else if (state.prefix == AjpRequestParser.CPONG) {
UndertowLogger.REQUEST_LOGGER.debug("Received CPONG, starting next request");
state = new AjpRequestParseState();
channel.getReadSetter().set(this);
channel.resumeReads();
} else {
UndertowLogger.REQUEST_LOGGER.ignoringAjpRequestWithPrefixCode(state.prefix);
safeClose(connection);
}
return;
}
// we remove ourselves as the read listener from the channel;
// if the http handler doesn't set any then reads will suspend, which is the right thing to do
channel.getReadSetter().set(null);
channel.suspendReads();
final HttpServerExchange httpServerExchange = this.httpServerExchange;
final AjpServerResponseConduit responseConduit = new AjpServerResponseConduit(connection.getChannel().getSinkChannel().getConduit(), connection.getByteBufferPool(), httpServerExchange, new ConduitListener<AjpServerResponseConduit>() {
@Override
public void handleEvent(AjpServerResponseConduit channel) {
Connectors.terminateResponse(httpServerExchange);
}
}, httpServerExchange.getRequestMethod().equals(Methods.HEAD));
connection.getChannel().getSinkChannel().setConduit(responseConduit);
connection.getChannel().getSourceChannel().setConduit(createSourceConduit(connection.getChannel().getSourceChannel().getConduit(), responseConduit, httpServerExchange));
//we need to set the write ready handler. This allows the response conduit to wrap it
responseConduit.setWriteReadyHandler(writeReadyHandler);
try {
connection.setSSLSessionInfo(state.createSslSessionInfo());
httpServerExchange.setSourceAddress(state.createPeerAddress());
httpServerExchange.setDestinationAddress(state.createDestinationAddress());
if(scheme != null) {
httpServerExchange.setRequestScheme(scheme);
}
if(state.attributes != null) {
httpServerExchange.putAttachment(HttpServerExchange.REQUEST_ATTRIBUTES, state.attributes);
}
AjpRequestParseState oldState = state;
state = null;
this.httpServerExchange = null;
httpServerExchange.setPersistent(true);
if(recordRequestStartTime) {
Connectors.setRequestStartTime(httpServerExchange);
}
connection.setCurrentExchange(httpServerExchange);
if(connectorStatistics != null) {
connectorStatistics.setup(httpServerExchange);
}
if(oldState.badRequest) {
httpServerExchange.setStatusCode(StatusCodes.BAD_REQUEST);
httpServerExchange.endExchange();
safeClose(connection);
} else {
Connectors.executeRootHandler(connection.getRootHandler(), httpServerExchange);
}
} catch (Throwable t) {
//TODO: we should attempt to return a 500 status code in this situation
UndertowLogger.REQUEST_LOGGER.exceptionProcessingRequest(t);
safeClose(connection);
}
} catch (BadRequestException e) {
UndertowLogger.REQUEST_IO_LOGGER.failedToParseRequest(e);
httpServerExchange.setStatusCode(StatusCodes.BAD_REQUEST);
httpServerExchange.endExchange();
safeClose(connection);
} catch (Exception e) {
UndertowLogger.REQUEST_LOGGER.exceptionProcessingRequest(e);
safeClose(connection);
} finally {
if (free) pooled.close();
}
}
private void handleCPing() {
state = new AjpRequestParseState();
final StreamConnection underlyingChannel = connection.getChannel();
underlyingChannel.getSourceChannel().suspendReads();
final ByteBuffer buffer = ByteBuffer.wrap(CPONG);
int res;
try {
do {
res = underlyingChannel.getSinkChannel().write(buffer);
if (res == 0) {
underlyingChannel.getSinkChannel().setWriteListener(new ChannelListener<ConduitStreamSinkChannel>() {
@Override
public void handleEvent(ConduitStreamSinkChannel channel) {
int res;
do {
try {
res = channel.write(buffer);
if (res == 0) {
return;
}
} catch (IOException e) {
UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
safeClose(connection);
}
} while (buffer.hasRemaining());
channel.suspendWrites();
AjpReadListener.this.handleEvent(underlyingChannel.getSourceChannel());
}
});
underlyingChannel.getSinkChannel().resumeWrites();
return;
}
} while (buffer.hasRemaining());
AjpReadListener.this.handleEvent(underlyingChannel.getSourceChannel());
} catch (IOException e) {
UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
safeClose(connection);
}
}
public void exchangeComplete(final HttpServerExchange exchange) {
if (!exchange.isUpgrade() && exchange.isPersistent()) {
startRequest();
ConduitStreamSourceChannel channel = ((AjpServerConnection) exchange.getConnection()).getChannel().getSourceChannel();
channel.getReadSetter().set(this);
channel.wakeupReads();
} else if(!exchange.isPersistent()) {
safeClose(exchange.getConnection());
}
}
private StreamSourceConduit createSourceConduit(StreamSourceConduit underlyingConduit, AjpServerResponseConduit responseConduit, final HttpServerExchange exchange) throws BadRequestException {
ReadDataStreamSourceConduit conduit = new ReadDataStreamSourceConduit(underlyingConduit, (AbstractServerConnection) exchange.getConnection());
final HeaderMap requestHeaders = exchange.getRequestHeaders();
HttpString transferEncoding = Headers.IDENTITY;
Long length;
final String teHeader = requestHeaders.getLast(Headers.TRANSFER_ENCODING);
boolean hasTransferEncoding = teHeader != null;
if (hasTransferEncoding) {
transferEncoding = new HttpString(teHeader);
}
final String requestContentLength = requestHeaders.getFirst(Headers.CONTENT_LENGTH);
if (hasTransferEncoding && !transferEncoding.equals(Headers.IDENTITY)) {
length = null; //unknown length
} else if (requestContentLength != null) {
try {
final long contentLength = Long.parseLong(requestContentLength);
if (contentLength == 0L) {
UndertowLogger.REQUEST_LOGGER.trace("No content, starting next request");
// no content - immediately start the next request, returning an empty stream for this one
Connectors.terminateRequest(httpServerExchange);
return new EmptyStreamSourceConduit(conduit.getReadThread());
} else {
length = contentLength;
}
} catch (NumberFormatException e) {
throw new BadRequestException("Invalid Content-Length header", e);
}
} else {
UndertowLogger.REQUEST_LOGGER.trace("No content length or transfer coding, starting next request");
// no content - immediately start the next request, returning an empty stream for this one
Connectors.terminateRequest(exchange);
return new EmptyStreamSourceConduit(conduit.getReadThread());
}
return new AjpServerRequestConduit(conduit, exchange, responseConduit, length, new ConduitListener<AjpServerRequestConduit>() {
@Override
public void handleEvent(AjpServerRequestConduit channel) {
Connectors.terminateRequest(exchange);
}
});
}
}