/*
* 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.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.apache.coyote.ActionCode;
import org.apache.coyote.CloseNowException;
import org.apache.coyote.Response;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.net.SocketWrapperBase;
import org.apache.tomcat.util.res.StringManager;
Provides buffering for the HTTP headers (allowing responses to be reset
before they have been committed) and the link to the Socket for writing the
headers (once committed) and the response body. Note that buffering of the
response body happens at a higher level.
/**
* Provides buffering for the HTTP headers (allowing responses to be reset
* before they have been committed) and the link to the Socket for writing the
* headers (once committed) and the response body. Note that buffering of the
* response body happens at a higher level.
*/
public class Http11OutputBuffer implements HttpOutputBuffer {
// -------------------------------------------------------------- Variables
The string manager for this package.
/**
* The string manager for this package.
*/
protected static final StringManager sm = StringManager.getManager(Http11OutputBuffer.class);
// ----------------------------------------------------- Instance Variables
Associated Coyote response.
/**
* Associated Coyote response.
*/
protected final Response response;
private volatile boolean ackSent = false;
Finished flag.
/**
* Finished flag.
*/
protected boolean responseFinished;
The buffer used for header composition.
/**
* The buffer used for header composition.
*/
protected final ByteBuffer headerBuffer;
Filter library for processing the response body.
/**
* Filter library for processing the response body.
*/
protected OutputFilter[] filterLibrary;
Active filters for the current request.
/**
* Active filters for the current request.
*/
protected OutputFilter[] activeFilters;
Index of the last active filter.
/**
* Index of the last active filter.
*/
protected int lastActiveFilter;
Underlying output buffer.
/**
* Underlying output buffer.
*/
protected HttpOutputBuffer outputStreamOutputBuffer;
Wrapper for socket where data will be written to.
/**
* Wrapper for socket where data will be written to.
*/
protected SocketWrapperBase<?> socketWrapper;
Bytes written to client for the current request
/**
* Bytes written to client for the current request
*/
protected long byteCount = 0;
protected Http11OutputBuffer(Response response, int headerBufferSize) {
this.response = response;
headerBuffer = ByteBuffer.allocate(headerBufferSize);
filterLibrary = new OutputFilter[0];
activeFilters = new OutputFilter[0];
lastActiveFilter = -1;
responseFinished = false;
outputStreamOutputBuffer = new SocketOutputBuffer();
}
// ------------------------------------------------------------- Properties
Add an output filter to the filter library. Note that calling this method
resets the currently active filters to none.
Params: - filter – The filter to add
/**
* Add an output filter to the filter library. Note that calling this method
* resets the currently active filters to none.
*
* @param filter The filter to add
*/
public void addFilter(OutputFilter filter) {
OutputFilter[] newFilterLibrary = Arrays.copyOf(filterLibrary, filterLibrary.length + 1);
newFilterLibrary[filterLibrary.length] = filter;
filterLibrary = newFilterLibrary;
activeFilters = new OutputFilter[filterLibrary.length];
}
Get filters.
Returns: The current filter library containing all possible filters
/**
* Get filters.
*
* @return The current filter library containing all possible filters
*/
public OutputFilter[] getFilters() {
return filterLibrary;
}
Add an output filter to the active filters for the current response.
The filter does not have to be present in getFilters()
.
A filter can only be added to a response once. If the filter has already
been added to this response then this method will be a NO-OP.
Params: - filter – The filter to add
/**
* Add an output filter to the active filters for the current response.
* <p>
* The filter does not have to be present in {@link #getFilters()}.
* <p>
* A filter can only be added to a response once. If the filter has already
* been added to this response then this method will be a NO-OP.
*
* @param filter The filter to add
*/
public void addActiveFilter(OutputFilter filter) {
if (lastActiveFilter == -1) {
filter.setBuffer(outputStreamOutputBuffer);
} else {
for (int i = 0; i <= lastActiveFilter; i++) {
if (activeFilters[i] == filter)
return;
}
filter.setBuffer(activeFilters[lastActiveFilter]);
}
activeFilters[++lastActiveFilter] = filter;
filter.setResponse(response);
}
// --------------------------------------------------- OutputBuffer Methods
@Override
public int doWrite(ByteBuffer chunk) throws IOException {
if (!response.isCommitted()) {
// Send the connector a request for commit. The connector should
// then validate the headers, send them (using sendHeaders) and
// set the filters accordingly.
response.action(ActionCode.COMMIT, null);
}
if (lastActiveFilter == -1) {
return outputStreamOutputBuffer.doWrite(chunk);
} else {
return activeFilters[lastActiveFilter].doWrite(chunk);
}
}
@Override
public long getBytesWritten() {
if (lastActiveFilter == -1) {
return outputStreamOutputBuffer.getBytesWritten();
} else {
return activeFilters[lastActiveFilter].getBytesWritten();
}
}
// ----------------------------------------------- HttpOutputBuffer Methods
Flush the response.
Throws: - IOException – an underlying I/O error occurred
/**
* Flush the response.
*
* @throws IOException an underlying I/O error occurred
*/
@Override
public void flush() throws IOException {
if (lastActiveFilter == -1) {
outputStreamOutputBuffer.flush();
} else {
activeFilters[lastActiveFilter].flush();
}
}
@Override
public void end() throws IOException {
if (responseFinished) {
return;
}
if (lastActiveFilter == -1) {
outputStreamOutputBuffer.end();
} else {
activeFilters[lastActiveFilter].end();
}
responseFinished = true;
}
// --------------------------------------------------------- Public Methods
Reset the header buffer if an error occurs during the writing of the
headers so the error response can be written.
/**
* Reset the header buffer if an error occurs during the writing of the
* headers so the error response can be written.
*/
void resetHeaderBuffer() {
headerBuffer.position(0).limit(headerBuffer.capacity());
}
Recycle the output buffer. This should be called when closing the
connection.
/**
* Recycle the output buffer. This should be called when closing the
* connection.
*/
public void recycle() {
nextRequest();
socketWrapper = null;
}
End processing of current HTTP request.
Note: All bytes of the current request should have been already
consumed. This method only resets all the pointers so that we are ready
to parse the next HTTP request.
/**
* End processing of current HTTP request.
* Note: All bytes of the current request should have been already
* consumed. This method only resets all the pointers so that we are ready
* to parse the next HTTP request.
*/
public void nextRequest() {
// Recycle filters
for (int i = 0; i <= lastActiveFilter; i++) {
activeFilters[i].recycle();
}
// Recycle response object
response.recycle();
// Reset pointers
headerBuffer.position(0).limit(headerBuffer.capacity());
lastActiveFilter = -1;
ackSent = false;
responseFinished = false;
byteCount = 0;
}
public void init(SocketWrapperBase<?> socketWrapper) {
this.socketWrapper = socketWrapper;
}
public void sendAck() throws IOException {
// It possible that the protocol configuration is changed between the
// request being received and the first read of the body. That could led
// to multiple calls to this method so ensure the ACK is only sent once.
if (!response.isCommitted() && !ackSent) {
ackSent = true;
socketWrapper.write(isBlocking(), Constants.ACK_BYTES, 0, Constants.ACK_BYTES.length);
if (flushBuffer(true)) {
throw new IOException(sm.getString("iob.failedwrite.ack"));
}
}
}
Commit the response.
Throws: - IOException – an underlying I/O error occurred
/**
* Commit the response.
*
* @throws IOException an underlying I/O error occurred
*/
protected void commit() throws IOException {
response.setCommitted(true);
if (headerBuffer.position() > 0) {
// Sending the response header buffer
headerBuffer.flip();
try {
SocketWrapperBase<?> socketWrapper = this.socketWrapper;
if (socketWrapper != null) {
socketWrapper.write(isBlocking(), headerBuffer);
} else {
throw new CloseNowException(sm.getString("iob.failedwrite"));
}
} finally {
headerBuffer.position(0).limit(headerBuffer.capacity());
}
}
}
Send the response status line.
/**
* Send the response status line.
*/
public void sendStatus() {
// Write protocol name
write(Constants.HTTP_11_BYTES);
headerBuffer.put(Constants.SP);
// Write status code
int status = response.getStatus();
switch (status) {
case 200:
write(Constants._200_BYTES);
break;
case 400:
write(Constants._400_BYTES);
break;
case 404:
write(Constants._404_BYTES);
break;
default:
write(status);
}
headerBuffer.put(Constants.SP);
// The reason phrase is optional but the space before it is not. Skip
// sending the reason phrase. Clients should ignore it (RFC 7230) and it
// just wastes bytes.
headerBuffer.put(Constants.CR).put(Constants.LF);
}
Send a header.
Params: - name – Header name
- value – Header value
/**
* Send a header.
*
* @param name Header name
* @param value Header value
*/
public void sendHeader(MessageBytes name, MessageBytes value) {
write(name);
headerBuffer.put(Constants.COLON).put(Constants.SP);
write(value);
headerBuffer.put(Constants.CR).put(Constants.LF);
}
End the header block.
/**
* End the header block.
*/
public void endHeaders() {
headerBuffer.put(Constants.CR).put(Constants.LF);
}
This method will write the contents of the specified message bytes
buffer to the output stream, without filtering. This method is meant to
be used to write the response header.
Params: - mb – data to be written
/**
* This method will write the contents of the specified message bytes
* buffer to the output stream, without filtering. This method is meant to
* be used to write the response header.
*
* @param mb data to be written
*/
private void write(MessageBytes mb) {
if (mb.getType() != MessageBytes.T_BYTES) {
mb.toBytes();
ByteChunk bc = mb.getByteChunk();
// Need to filter out CTLs excluding TAB. ISO-8859-1 and UTF-8
// values will be OK. Strings using other encodings may be
// corrupted.
byte[] buffer = bc.getBuffer();
for (int i = bc.getOffset(); i < bc.getLength(); i++) {
// byte values are signed i.e. -128 to 127
// The values are used unsigned. 0 to 31 are CTLs so they are
// filtered (apart from TAB which is 9). 127 is a control (DEL).
// The values 128 to 255 are all OK. Converting those to signed
// gives -128 to -1.
if ((buffer[i] > -1 && buffer[i] <= 31 && buffer[i] != 9) ||
buffer[i] == 127) {
buffer[i] = ' ';
}
}
}
write(mb.getByteChunk());
}
This method will write the contents of the specified byte chunk to the
output stream, without filtering. This method is meant to be used to
write the response header.
Params: - bc – data to be written
/**
* This method will write the contents of the specified byte chunk to the
* output stream, without filtering. This method is meant to be used to
* write the response header.
*
* @param bc data to be written
*/
private void write(ByteChunk bc) {
// Writing the byte chunk to the output buffer
int length = bc.getLength();
checkLengthBeforeWrite(length);
headerBuffer.put(bc.getBytes(), bc.getStart(), length);
}
This method will write the contents of the specified byte
buffer to the output stream, without filtering. This method is meant to
be used to write the response header.
Params: - b – data to be written
/**
* This method will write the contents of the specified byte
* buffer to the output stream, without filtering. This method is meant to
* be used to write the response header.
*
* @param b data to be written
*/
public void write(byte[] b) {
checkLengthBeforeWrite(b.length);
// Writing the byte chunk to the output buffer
headerBuffer.put(b);
}
This method will write the specified integer to the output stream. This
method is meant to be used to write the response header.
Params: - value – data to be written
/**
* This method will write the specified integer to the output stream. This
* method is meant to be used to write the response header.
*
* @param value data to be written
*/
private void write(int value) {
// From the Tomcat 3.3 HTTP/1.0 connector
String s = Integer.toString(value);
int len = s.length();
checkLengthBeforeWrite(len);
for (int i = 0; i < len; i++) {
char c = s.charAt (i);
headerBuffer.put((byte) c);
}
}
Checks to see if there is enough space in the buffer to write the
requested number of bytes.
/**
* Checks to see if there is enough space in the buffer to write the
* requested number of bytes.
*/
private void checkLengthBeforeWrite(int length) {
// "+ 4": BZ 57509. Reserve space for CR/LF/COLON/SP characters that
// are put directly into the buffer following this write operation.
if (headerBuffer.position() + length + 4 > headerBuffer.capacity()) {
throw new HeadersTooLargeException(
sm.getString("iob.responseheadertoolarge.error"));
}
}
//------------------------------------------------------ Non-blocking writes
Writes any remaining buffered data.
Params: - block – Should this method block until the buffer is empty
Throws: - IOException – Error writing data
Returns: true
if data remains in the buffer (which can only
happen in non-blocking mode) else false
.
/**
* Writes any remaining buffered data.
*
* @param block Should this method block until the buffer is empty
* @return <code>true</code> if data remains in the buffer (which can only
* happen in non-blocking mode) else <code>false</code>.
* @throws IOException Error writing data
*/
protected boolean flushBuffer(boolean block) throws IOException {
return socketWrapper.flush(block);
}
Is standard Servlet blocking IO being used for output?
Returns: true
if this is blocking IO
/**
* Is standard Servlet blocking IO being used for output?
* @return <code>true</code> if this is blocking IO
*/
protected final boolean isBlocking() {
return response.getWriteListener() == null;
}
protected final boolean isReady() {
boolean result = !hasDataToWrite();
if (!result) {
socketWrapper.registerWriteInterest();
}
return result;
}
public boolean hasDataToWrite() {
return socketWrapper.hasDataToWrite();
}
public void registerWriteInterest() {
socketWrapper.registerWriteInterest();
}
boolean isChunking() {
for (int i = 0; i < lastActiveFilter; i++) {
if (activeFilters[i] == filterLibrary[Constants.CHUNKED_FILTER]) {
return true;
}
}
return false;
}
// ------------------------------------------ SocketOutputBuffer Inner Class
This class is an output buffer which will write data to a socket.
/**
* This class is an output buffer which will write data to a socket.
*/
protected class SocketOutputBuffer implements HttpOutputBuffer {
Write chunk.
/**
* Write chunk.
*/
@Override
public int doWrite(ByteBuffer chunk) throws IOException {
try {
int len = chunk.remaining();
SocketWrapperBase<?> socketWrapper = Http11OutputBuffer.this.socketWrapper;
if (socketWrapper != null) {
socketWrapper.write(isBlocking(), chunk);
} else {
throw new CloseNowException(sm.getString("iob.failedwrite"));
}
len -= chunk.remaining();
byteCount += len;
return len;
} catch (IOException ioe) {
response.action(ActionCode.CLOSE_NOW, ioe);
// Re-throw
throw ioe;
}
}
@Override
public long getBytesWritten() {
return byteCount;
}
@Override
public void end() throws IOException {
socketWrapper.flush(true);
}
@Override
public void flush() throws IOException {
socketWrapper.flush(isBlocking());
}
}
}