blob: f9f20d970cdfe9a9db40dde25faf6176b8b5bc8c [file] [log] [blame]
/*
* 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.http11;
import java.io.EOFException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import org.apache.coyote.InputBuffer;
import org.apache.coyote.Request;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.jni.Socket;
import org.apache.tomcat.jni.Status;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.net.AbstractEndpoint;
import org.apache.tomcat.util.net.SocketWrapperBase;
/**
* Implementation of InputBuffer which provides HTTP request header parsing as
* well as transfer decoding.
*
* @author <a href="mailto:remm@apache.org">Remy Maucherat</a>
*/
public class InternalAprInputBuffer extends AbstractInputBuffer<Long> {
private static final Log log =
LogFactory.getLog(InternalAprInputBuffer.class);
// ----------------------------------------------------------- Constructors
/**
* Alternate constructor.
*/
public InternalAprInputBuffer(Request request, int headerBufferSize) {
this.request = request;
headers = request.getMimeHeaders();
buf = new byte[headerBufferSize];
if (headerBufferSize < (8 * 1024)) {
bbuf = ByteBuffer.allocateDirect(6 * 1500);
} else {
bbuf = ByteBuffer.allocateDirect((headerBufferSize / 1500 + 1) * 1500);
}
inputStreamInputBuffer = new SocketInputBuffer();
filterLibrary = new InputFilter[0];
activeFilters = new InputFilter[0];
lastActiveFilter = -1;
parsingHeader = true;
swallowInput = true;
}
// ----------------------------------------------------- Instance Variables
/**
* Direct byte buffer used to perform actual reading.
*/
private final ByteBuffer bbuf;
/**
* Underlying socket.
*/
private long socket;
private SocketWrapperBase<Long> wrapper;
// --------------------------------------------------------- Public Methods
/**
* Recycle the input buffer. This should be called when closing the
* connection.
*/
@Override
public void recycle() {
socket = 0;
wrapper = null;
super.recycle();
}
/**
* Read the request line. This function is meant to be used during the
* HTTP request header parsing. Do NOT attempt to read the request body
* using it.
*
* @throws IOException If an exception occurs during the underlying socket
* read operations, or if the given buffer is not big enough to accommodate
* the whole line.
* @return true if data is properly fed; false if no data is available
* immediately and thread should be freed
*/
@Override
public boolean parseRequestLine(boolean useAvailableData)
throws IOException {
int start = 0;
//
// Skipping blank lines
//
byte chr = 0;
do {
// Read new bytes if needed
if (pos >= lastValid) {
if (useAvailableData) {
return false;
}
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
// Set the start time once we start reading data (even if it is
// just skipping blank lines)
if (request.getStartTime() < 0) {
request.setStartTime(System.currentTimeMillis());
}
chr = buf[pos++];
} while ((chr == Constants.CR) || (chr == Constants.LF));
pos--;
// Mark the current buffer position
start = pos;
if (pos >= lastValid) {
if (useAvailableData) {
return false;
}
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
//
// Reading the method name
// Method name is always US-ASCII
//
boolean space = false;
while (!space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
// Spec says no CR or LF in method name
if (buf[pos] == Constants.CR || buf[pos] == Constants.LF) {
throw new IllegalArgumentException(
sm.getString("iib.invalidmethod"));
}
// Spec says single SP but it also says be tolerant of HT
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
space = true;
request.method().setBytes(buf, start, pos - start);
}
pos++;
}
// Spec says single SP but also says be tolerant of multiple and/or HT
while (space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
pos++;
} else {
space = false;
}
}
// Mark the current buffer position
start = pos;
int end = 0;
int questionPos = -1;
//
// Reading the URI
//
boolean eol = false;
while (!space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
// Spec says single SP but it also says be tolerant of HT
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
space = true;
end = pos;
} else if ((buf[pos] == Constants.CR)
|| (buf[pos] == Constants.LF)) {
// HTTP/0.9 style request
eol = true;
space = true;
end = pos;
} else if ((buf[pos] == Constants.QUESTION)
&& (questionPos == -1)) {
questionPos = pos;
}
pos++;
}
if (questionPos >= 0) {
request.queryString().setBytes(buf, questionPos + 1,
end - questionPos - 1);
request.requestURI().setBytes(buf, start, questionPos - start);
} else {
request.requestURI().setBytes(buf, start, end - start);
}
// Spec says single SP but also says be tolerant of multiple and/or HT
while (space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
pos++;
} else {
space = false;
}
}
// Mark the current buffer position
start = pos;
end = 0;
//
// Reading the protocol
// Protocol is always US-ASCII
//
while (!eol) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.CR) {
end = pos;
} else if (buf[pos] == Constants.LF) {
if (end == 0)
end = pos;
eol = true;
}
pos++;
}
if ((end - start) > 0) {
request.protocol().setBytes(buf, start, end - start);
} else {
request.protocol().setString("");
}
return true;
}
/**
* Parse the HTTP headers.
*/
@Override
public boolean parseHeaders()
throws IOException {
if (!parsingHeader) {
throw new IllegalStateException(
sm.getString("iib.parseheaders.ise.error"));
}
while (parseHeader()) {
// Loop until there are no more headers
}
parsingHeader = false;
end = pos;
return true;
}
/**
* Parse an HTTP header.
*
* @return false after reading a blank line (which indicates that the
* HTTP header parsing is done
*/
@SuppressWarnings("null") // headerValue cannot be null
private boolean parseHeader()
throws IOException {
//
// Check for blank line
//
byte chr = 0;
while (true) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
chr = buf[pos];
if (chr == Constants.CR) {
// Skip
} else if (chr == Constants.LF) {
pos++;
return false;
} else {
break;
}
pos++;
}
// Mark the current buffer position
int start = pos;
//
// Reading the header name
// Header name is always US-ASCII
//
boolean colon = false;
MessageBytes headerValue = null;
while (!colon) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.COLON) {
colon = true;
headerValue = headers.addValue(buf, start, pos - start);
} else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
// If a non-token header is detected, skip the line and
// ignore the header
skipLine(start);
return true;
}
chr = buf[pos];
if ((chr >= Constants.A) && (chr <= Constants.Z)) {
buf[pos] = (byte) (chr - Constants.LC_OFFSET);
}
pos++;
}
// Mark the current buffer position
start = pos;
int realPos = pos;
//
// Reading the header value (which can be spanned over multiple lines)
//
boolean eol = false;
boolean validLine = true;
while (validLine) {
boolean space = true;
// Skipping spaces
while (space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
if ((buf[pos] == Constants.SP) || (buf[pos] == Constants.HT)) {
pos++;
} else {
space = false;
}
}
int lastSignificantChar = realPos;
// Reading bytes until the end of the line
while (!eol) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.CR) {
// Skip
} else if (buf[pos] == Constants.LF) {
eol = true;
} else if (buf[pos] == Constants.SP) {
buf[realPos] = buf[pos];
realPos++;
} else {
buf[realPos] = buf[pos];
realPos++;
lastSignificantChar = realPos;
}
pos++;
}
realPos = lastSignificantChar;
// Checking the first character of the new line. If the character
// is a LWS, then it's a multiline header
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
chr = buf[pos];
if ((chr != Constants.SP) && (chr != Constants.HT)) {
validLine = false;
} else {
eol = false;
// Copying one extra space in the buffer (since there must
// be at least one space inserted between the lines)
buf[realPos] = chr;
realPos++;
}
}
// Set the header value
headerValue.setBytes(buf, start, realPos - start);
return true;
}
private void skipLine(int start) throws IOException {
boolean eol = false;
int lastRealByte = start;
if (pos - 1 > start) {
lastRealByte = pos - 1;
}
while (!eol) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill(true))
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.CR) {
// Skip
} else if (buf[pos] == Constants.LF) {
eol = true;
} else {
lastRealByte = pos;
}
pos++;
}
if (log.isDebugEnabled()) {
log.debug(sm.getString("iib.invalidheader", new String(buf, start,
lastRealByte - start + 1, StandardCharsets.ISO_8859_1)));
}
}
// ---------------------------------------------------- InputBuffer Methods
/**
* Read some bytes.
*/
@Override
public int doRead(ByteChunk chunk, Request req)
throws IOException {
if (lastActiveFilter == -1)
return inputStreamInputBuffer.doRead(chunk, req);
else
return activeFilters[lastActiveFilter].doRead(chunk,req);
}
// ------------------------------------------------------ Protected Methods
@Override
protected void init(SocketWrapperBase<Long> socketWrapper,
AbstractEndpoint<Long> endpoint) throws IOException {
socket = socketWrapper.getSocket().longValue();
wrapper = socketWrapper;
Socket.setrbb(this.socket, bbuf);
}
@Override
protected boolean fill(boolean block) throws IOException {
int nRead = 0;
if (parsingHeader) {
if (lastValid == buf.length) {
throw new IllegalArgumentException
(sm.getString("iib.requestheadertoolarge.error"));
}
} else {
if (buf.length - end < 4500) {
// In this case, the request header was really large, so we allocate a
// brand new one; the old one will get GCed when subsequent requests
// clear all references
buf = new byte[buf.length];
end = 0;
}
pos = end;
lastValid = pos;
}
bbuf.clear();
nRead = doReadSocket(block);
if (nRead > 0) {
bbuf.limit(nRead);
bbuf.get(buf, pos, nRead);
lastValid = pos + nRead;
} else if (-nRead == Status.EAGAIN) {
return false;
} else if ((-nRead) == Status.ETIMEDOUT || (-nRead) == Status.TIMEUP) {
if (block) {
throw new SocketTimeoutException(
sm.getString("iib.readtimeout"));
} else {
// Attempting to read from the socket when the poller
// has not signalled that there is data to read appears
// to behave like a blocking read with a short timeout
// on OSX rather than like a non-blocking read. If no
// data is read, treat the resulting timeout like a
// non-blocking read that returned no data.
return false;
}
} else if (nRead == 0) {
// APR_STATUS_IS_EOF, since native 1.1.22
return false;
} else {
throw new IOException(sm.getString("iib.failedread.apr",
Integer.valueOf(-nRead)));
}
return (nRead > 0);
}
@Override
protected final Log getLog() {
return log;
}
private int doReadSocket(boolean block) {
Lock readLock = wrapper.getBlockingStatusReadLock();
WriteLock writeLock = wrapper.getBlockingStatusWriteLock();
boolean readDone = false;
int result = 0;
readLock.lock();
try {
if (wrapper.getBlockingStatus() == block) {
result = Socket.recvbb(socket, 0, buf.length - lastValid);
readDone = true;
}
} finally {
readLock.unlock();
}
if (!readDone) {
writeLock.lock();
try {
wrapper.setBlockingStatus(block);
// Set the current settings for this socket
if (block) {
Socket.optSet(socket, Socket.APR_SO_NONBLOCK, 0);
} else {
Socket.optSet(socket, Socket.APR_SO_NONBLOCK, 1);
Socket.timeoutSet(socket, 0);
}
// Downgrade the lock
readLock.lock();
try {
writeLock.unlock();
result = Socket.recvbb(socket, 0, buf.length - lastValid);
} finally {
readLock.unlock();
}
} finally {
// Should have been released above but may not have been on some
// exception paths
if (writeLock.isHeldByCurrentThread()) {
writeLock.unlock();
}
}
}
return result;
}
// ------------------------------------- InputStreamInputBuffer Inner Class
/**
* This class is an input buffer which will read its data from an input
* stream.
*/
protected class SocketInputBuffer
implements InputBuffer {
/**
* Read bytes into the specified chunk.
*/
@Override
public int doRead(ByteChunk chunk, Request req )
throws IOException {
if (pos >= lastValid) {
if (!fill(true))
return -1;
}
int length = lastValid - pos;
chunk.setBytes(buf, pos, length);
pos = lastValid;
return (length);
}
}
}