/* * 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 org.apache.coyote.ActionCode; import org.apache.coyote.Response; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.MessageBytes; import org.apache.tomcat.util.http.HttpMessages; 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. */ public class Http11OutputBuffer implements HttpOutputBuffer { // -------------------------------------------------------------- Variables /** * The string manager for this package. */ protected static final StringManager sm = StringManager.getManager(Http11OutputBuffer.class); // ----------------------------------------------------- Instance Variables /** * Associated Coyote response. */ protected Response response; /** * Finished flag. */ protected boolean responseFinished; /** * The buffer used for header composition. */ protected final ByteBuffer headerBuffer; /** * Filter library for processing the response body. */ protected OutputFilter[] filterLibrary; /** * Active filters for the current request. */ protected OutputFilter[] activeFilters; /** * Index of the last active filter. */ protected int lastActiveFilter; /** * Underlying output buffer. */ protected HttpOutputBuffer outputStreamOutputBuffer; /** * Wrapper for socket where data will be written to. */ protected SocketWrapperBase> socketWrapper; /** * Bytes written to client for the current request */ protected long byteCount = 0; @Deprecated private boolean sendReasonPhrase = false; protected Http11OutputBuffer(Response response, int headerBufferSize, boolean sendReasonPhrase) { this.response = response; this.sendReasonPhrase = sendReasonPhrase; headerBuffer = ByteBuffer.allocate(headerBufferSize); filterLibrary = new OutputFilter[0]; activeFilters = new OutputFilter[0]; lastActiveFilter = -1; responseFinished = false; outputStreamOutputBuffer = new SocketOutputBuffer(); if (sendReasonPhrase) { // Cause loading of HttpMessages HttpMessages.getInstance(response.getLocale()).getMessage(200); } } // ------------------------------------------------------------- Properties /** * 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 = new OutputFilter[filterLibrary.length + 1]; for (int i = 0; i < filterLibrary.length; i++) { newFilterLibrary[i] = filterLibrary[i]; } newFilterLibrary[filterLibrary.length] = filter; filterLibrary = newFilterLibrary; activeFilters = new OutputFilter[filterLibrary.length]; } /** * 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 {@link #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.
*
* @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
/**
* @deprecated Unused. Will be removed in Tomcat 9. Use
* {@link #doWrite(ByteBuffer)}
*/
@Deprecated
@Override
public int doWrite(ByteChunk 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 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
*/
@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.
*/
void resetHeaderBuffer() {
headerBuffer.position(0).limit(headerBuffer.capacity());
}
/**
* 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.
*/
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;
responseFinished = false;
byteCount = 0;
}
public void init(SocketWrapperBase> socketWrapper) {
this.socketWrapper = socketWrapper;
}
@SuppressWarnings("deprecation")
public void sendAck() throws IOException {
if (!response.isCommitted()) {
if (sendReasonPhrase) {
socketWrapper.write(isBlocking(), Constants.ACK_BYTES_REASON, 0, Constants.ACK_BYTES_REASON.length);
} else {
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
*/
protected void commit() throws IOException {
response.setCommitted(true);
if (headerBuffer.position() > 0) {
// Sending the response header buffer
headerBuffer.flip();
try {
socketWrapper.write(isBlocking(), headerBuffer);
} finally {
headerBuffer.position(0).limit(headerBuffer.capacity());
}
}
}
/**
* Send the response status line.
*/
@SuppressWarnings("deprecation")
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);
if (sendReasonPhrase) {
// Write message
String message = null;
if (org.apache.coyote.Constants.USE_CUSTOM_STATUS_MSG_IN_HEADER &&
HttpMessages.isSafeInHttpHeader(response.getMessage())) {
message = response.getMessage();
}
if (message == null) {
write(HttpMessages.getInstance(
response.getLocale()).getMessage(status));
} else {
write(message);
}
} else {
// 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.
*
* @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.
*/
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.
*
* @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.
*
* @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.
*
* @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 contents of the specified String to the
* output stream, without filtering. This method is meant to be used to
* write the response header.
*
* @param s data to be written
*/
private void write(String s) {
if (s == null) {
return;
}
// From the Tomcat 3.3 HTTP/1.0 connector
int len = s.length();
checkLengthBeforeWrite(len);
for (int i = 0; i < len; i++) {
char c = s.charAt (i);
// Note: This is clearly incorrect for many strings,
// but is the only consistent approach within the current
// servlet framework. It must suffice until servlet output
// streams properly encode their output.
if (((c <= 31) && (c != 9)) || c == 127 || c > 255) {
c = ' ';
}
headerBuffer.put((byte) c);
}
}
/**
* 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.
*/
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.
*
* @param block Should this method block until the buffer is empty
* @return true if data remains in the buffer (which can only
* happen in non-blocking mode) else false.
* @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?
* @return true 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();
}
// ------------------------------------------ SocketOutputBuffer Inner Class
/**
* This class is an output buffer which will write data to a socket.
*/
protected class SocketOutputBuffer implements HttpOutputBuffer {
/**
* Write chunk.
*
* @deprecated Unused. Will be removed in Tomcat 9. Use
* {@link #doWrite(ByteBuffer)}
*/
@Deprecated
@Override
public int doWrite(ByteChunk chunk) throws IOException {
int len = chunk.getLength();
int start = chunk.getStart();
byte[] b = chunk.getBuffer();
socketWrapper.write(isBlocking(), b, start, len);
byteCount += len;
return len;
}
/**
* Write chunk.
*/
@Override
public int doWrite(ByteBuffer chunk) throws IOException {
try {
int len = chunk.remaining();
socketWrapper.write(isBlocking(), chunk);
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());
}
}
}