All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.undertow.server.protocol.http.HttpResponseConduit Maven / Gradle / Ivy

There is a newer version: 2.3.18.Final
Show newest version
/*
 * 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.http;

import io.undertow.UndertowMessages;
import io.undertow.connector.ByteBufferPool;
import io.undertow.connector.PooledByteBuffer;
import io.undertow.server.Connectors;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HeaderMap;
import io.undertow.util.HeaderValues;
import io.undertow.util.HttpString;
import io.undertow.util.Protocols;
import io.undertow.util.StatusCodes;
import org.xnio.Buffers;
import org.xnio.IoUtils;
import org.xnio.XnioWorker;
import org.xnio.channels.StreamSourceChannel;
import org.xnio.conduits.AbstractStreamSinkConduit;
import org.xnio.conduits.ConduitWritableByteChannel;
import org.xnio.conduits.Conduits;
import org.xnio.conduits.StreamSinkConduit;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;

import static org.xnio.Bits.allAreClear;
import static org.xnio.Bits.allAreSet;
import static org.xnio.Bits.anyAreSet;

/**
 * Conduit for writing the HTTP response.
 *
 * @author David M. Lloyd
 * @author Flavia Rainone
 */
final class HttpResponseConduit extends AbstractStreamSinkConduit {

    private final ByteBufferPool pool;
    private final HttpServerConnection connection;

    private int state = STATE_START;

    private long fiCookie = -1L;
    private String string;
    private HeaderValues headerValues;
    private int valueIdx;
    private int charIndex;
    private PooledByteBuffer pooledBuffer;
    private PooledByteBuffer pooledFileTransferBuffer;
    private HttpServerExchange exchange;

    private ByteBuffer[] writevBuffer;
    private boolean done = false;

    private static final int STATE_BODY = 0; // Message body, normal pass-through operation
    private static final int STATE_START = 1; // No headers written yet
    private static final int STATE_HDR_NAME = 2; // Header name indexed by charIndex
    private static final int STATE_HDR_D = 3; // Header delimiter ':'
    private static final int STATE_HDR_DS = 4; // Header delimiter ': '
    private static final int STATE_HDR_VAL = 5; // Header value
    private static final int STATE_HDR_EOL_CR = 6; // Header line CR
    private static final int STATE_HDR_EOL_LF = 7; // Header line LF
    private static final int STATE_HDR_FINAL_CR = 8; // Final CR
    private static final int STATE_HDR_FINAL_LF = 9; // Final LF
    private static final int STATE_BUF_FLUSH = 10; // flush the buffer and go to writing body
    private static final int POOLED_BUFFER_IN_USE = 1 << 5; // can occur on recursive writes
    // (the recursive write will happen when an outer channel/conduit needs to write to handle a close on write),
    // POOLED_BUFFER_IN_USE can occur concomitantly with more than one of the states above (hence the flag instead of a new state)

    private static final int MASK_STATE = 0x0000000F;
    private static final int FLAG_SHUTDOWN = 0x00000010;

    HttpResponseConduit(final StreamSinkConduit next, final ByteBufferPool pool, HttpServerConnection connection) {
        super(next);
        this.pool = pool;
        this.connection = connection;
    }

    HttpResponseConduit(final StreamSinkConduit next, final ByteBufferPool pool, HttpServerConnection connection, HttpServerExchange exchange) {
        super(next);
        this.pool = pool;
        this.connection = connection;
        this.exchange = exchange;
    }
    void reset(HttpServerExchange exchange) {

        this.exchange = exchange;
        state = STATE_START;
        fiCookie = -1L;
        string = null;
        headerValues = null;
        valueIdx = 0;
        charIndex = 0;
    }

    /**
     * Handles writing out the header data. It can also take a byte buffer of user
     * data, to enable both user data and headers to be written out in a single operation,
     * which has a noticeable performance impact.
     * 

* It is up to the caller to note the current position of this buffer before and after they * call this method, and use this to figure out how many bytes (if any) have been written. * * @param state * @param userData * @return * @throws IOException */ private int processWrite(int state, final Object userData, int pos, int length) throws IOException { if (done || exchange == null) { throw new ClosedChannelException(); } ByteBuffer buffer = null; try { assert state != STATE_BODY; if (state == STATE_BUF_FLUSH) { buffer = pooledBuffer.getBuffer(); do { long res = 0; ByteBuffer[] data; if (userData == null || length == 0) { res = next.write(buffer); } else if (userData instanceof ByteBuffer) { data = writevBuffer; if (data == null) { data = writevBuffer = new ByteBuffer[2]; } data[0] = buffer; data[1] = (ByteBuffer) userData; res = next.write(data, 0, 2); } else { data = writevBuffer; if (data == null || data.length < length + 1) { data = writevBuffer = new ByteBuffer[length + 1]; } data[0] = buffer; System.arraycopy(userData, pos, data, 1, length); res = next.write(data, 0, length + 1); } if (res == 0) { return STATE_BUF_FLUSH; } } while (buffer.hasRemaining()); return STATE_BODY; } else if (state != STATE_START) { return processStatefulWrite(state, userData, pos, length); } // make sure that headers are written only once. if // pooled buffer is in use, it is a sign the headers are being processed // by an outer call at the stack if (!anyAreSet(this.state, POOLED_BUFFER_IN_USE)) { //merge the cookies into the header map Connectors.flattenCookies(exchange); // allocate pooled buffer if (pooledBuffer == null) { pooledBuffer = pool.allocate(); } buffer = pooledBuffer.getBuffer(); // set the state after successfully allocating... so in case something goes bad // we don't have a dangling flag that won't be cleared at the finally block this.state |= POOLED_BUFFER_IN_USE; assert buffer.remaining() >= 50; // append response status and headers Protocols.HTTP_1_1.appendTo(buffer); buffer.put((byte) ' '); int code = exchange.getStatusCode(); assert 999 >= code && code >= 100; buffer.put((byte) (code / 100 + '0')); buffer.put((byte) (code / 10 % 10 + '0')); buffer.put((byte) (code % 10 + '0')); buffer.put((byte) ' '); String string = exchange.getReasonPhrase(); if (string == null) { string = StatusCodes.getReason(code); } if (string.length() > buffer.remaining()) { pooledBuffer.close(); pooledBuffer = null; truncateWrites(); throw UndertowMessages.MESSAGES.reasonPhraseToLargeForBuffer(string); } writeString(buffer, string); buffer.put((byte) '\r').put((byte) '\n'); int remaining = buffer.remaining(); final HeaderMap headers = exchange.getResponseHeaders(); long fiCookie = headers.fastIterateNonEmpty(); while (fiCookie != -1) { HeaderValues headerValues = headers.fiCurrent(fiCookie); HttpString header = headerValues.getHeaderName(); int headerSize = header.length(); int valueIdx = 0; while (valueIdx < headerValues.size()) { remaining -= (headerSize + 2); if (remaining < 0) { this.fiCookie = fiCookie; this.string = string; this.headerValues = headerValues; this.valueIdx = valueIdx; this.charIndex = 0; this.state = STATE_HDR_NAME; buffer.flip(); return processStatefulWrite(STATE_HDR_NAME, userData, pos, length); } header.appendTo(buffer); buffer.put((byte) ':').put((byte) ' '); string = headerValues.get(valueIdx++); remaining -= (string.length() + 2); if (remaining < 2) {//we use 2 here, to make sure we always have room for the final \r\n this.fiCookie = fiCookie; this.string = string; this.headerValues = headerValues; this.valueIdx = valueIdx; this.charIndex = 0; this.state = STATE_HDR_VAL; buffer.flip(); return processStatefulWrite(STATE_HDR_VAL, userData, pos, length); } writeString(buffer, string); buffer.put((byte) '\r').put((byte) '\n'); } fiCookie = headers.fiNextNonEmpty(fiCookie); } buffer.put((byte) '\r').put((byte) '\n'); buffer.flip(); } // now write everything ByteBuffer[] data = null; do { long res = 0; if (userData == null) { if (buffer != null) { res = next.write(buffer); } } else if (userData instanceof ByteBuffer) { data = writevBuffer; if (data == null) { data = writevBuffer = new ByteBuffer[2]; } int index = 0; if (buffer != null) { data[index++] = buffer; } data[index++] = (ByteBuffer) userData; res = next.write(data, 0, index); } else { data = writevBuffer; if (data == null || data.length < length + 1) { data = writevBuffer = new ByteBuffer[length + 1]; } int index = 0; if (buffer != null) { data[index++] = buffer; } System.arraycopy(userData, pos, data, index, length); res = next.write(data, 0, index + length); } if (res == 0) { return STATE_BUF_FLUSH; } } while (buffer != null && buffer.hasRemaining()); return STATE_BODY; } finally { if (buffer != null) { bufferDone(); this.state &= ~POOLED_BUFFER_IN_USE; } } } private void bufferDone() { if(exchange == null) { return; } HttpServerConnection connection = (HttpServerConnection)exchange.getConnection(); if(connection.getExtraBytes() != null && connection.isOpen() && exchange.isRequestComplete()) { //if we are pipelining we hold onto the buffer pooledBuffer.getBuffer().clear(); } else { pooledBuffer.close(); pooledBuffer = null; this.exchange = null; } } public void freeContinueResponse() { if (pooledBuffer != null) { pooledBuffer.close(); pooledBuffer = null; } } private static void writeString(ByteBuffer buffer, String string) { int length = string.length(); for (int charIndex = 0; charIndex < length; charIndex++) { char c = string.charAt(charIndex); byte b = (byte) c; if(b != '\r' && b != '\n') { buffer.put(b); } else { buffer.put((byte) ' '); } } } /** * Handles writing out the header data in the case where is is too big to fit into a buffer. This is a much slower code path. */ private int processStatefulWrite(int state, final Object userData, int pos, int len) throws IOException { ByteBuffer buffer = pooledBuffer.getBuffer(); long fiCookie = this.fiCookie; int valueIdx = this.valueIdx; int charIndex = this.charIndex; int length; String string = this.string; HeaderValues headerValues = this.headerValues; int res; // BUFFER IS FLIPPED COMING IN if (buffer.hasRemaining()) { do { res = next.write(buffer); if (res == 0) { return state; } } while (buffer.hasRemaining()); } buffer.clear(); HeaderMap headers = exchange.getResponseHeaders(); // BUFFER IS NOW EMPTY FOR FILLING for (; ; ) { switch (state) { case STATE_HDR_NAME: { final HttpString headerName = headerValues.getHeaderName(); length = headerName.length(); while (charIndex < length) { if (buffer.hasRemaining()) { buffer.put(headerName.byteAt(charIndex++)); } else { buffer.flip(); do { res = next.write(buffer); if (res == 0) { this.string = string; this.headerValues = headerValues; this.charIndex = charIndex; this.fiCookie = fiCookie; this.valueIdx = valueIdx; return STATE_HDR_NAME; } } while (buffer.hasRemaining()); buffer.clear(); } } // fall thru } case STATE_HDR_D: { if (!buffer.hasRemaining()) { buffer.flip(); do { res = next.write(buffer); if (res == 0) { this.string = string; this.headerValues = headerValues; this.charIndex = charIndex; this.fiCookie = fiCookie; this.valueIdx = valueIdx; return STATE_HDR_D; } } while (buffer.hasRemaining()); buffer.clear(); } buffer.put((byte) ':'); // fall thru } case STATE_HDR_DS: { if (!buffer.hasRemaining()) { buffer.flip(); do { res = next.write(buffer); if (res == 0) { this.string = string; this.headerValues = headerValues; this.charIndex = charIndex; this.fiCookie = fiCookie; this.valueIdx = valueIdx; return STATE_HDR_DS; } } while (buffer.hasRemaining()); buffer.clear(); } buffer.put((byte) ' '); string = headerValues.get(valueIdx++); charIndex = 0; // fall thru } case STATE_HDR_VAL: { length = string.length(); while (charIndex < length) { if (buffer.hasRemaining()) { buffer.put((byte) string.charAt(charIndex++)); } else { buffer.flip(); do { res = next.write(buffer); if (res == 0) { this.string = string; this.headerValues = headerValues; this.charIndex = charIndex; this.fiCookie = fiCookie; this.valueIdx = valueIdx; return STATE_HDR_VAL; } } while (buffer.hasRemaining()); buffer.clear(); } } charIndex = 0; if (valueIdx == headerValues.size()) { if (!buffer.hasRemaining()) { if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) return STATE_HDR_EOL_CR; } buffer.put((byte) 13); // CR if (!buffer.hasRemaining()) { if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) return STATE_HDR_EOL_LF; } buffer.put((byte) 10); // LF if ((fiCookie = headers.fiNextNonEmpty(fiCookie)) != -1L) { headerValues = headers.fiCurrent(fiCookie); valueIdx = 0; state = STATE_HDR_NAME; break; } else { if (!buffer.hasRemaining()) { if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) return STATE_HDR_FINAL_CR; } buffer.put((byte) 13); // CR if (!buffer.hasRemaining()) { if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) return STATE_HDR_FINAL_LF; } buffer.put((byte) 10); // LF this.fiCookie = -1; this.valueIdx = 0; this.string = null; buffer.flip(); //for performance reasons we use a gather write if there is user data if (userData == null) { do { res = next.write(buffer); if (res == 0) { return STATE_BUF_FLUSH; } } while (buffer.hasRemaining()); } else if(userData instanceof ByteBuffer) { ByteBuffer[] b = {buffer, (ByteBuffer) userData}; do { long r = next.write(b, 0, b.length); if (r == 0 && buffer.hasRemaining()) { return STATE_BUF_FLUSH; } } while (buffer.hasRemaining()); } else { ByteBuffer[] b = new ByteBuffer[1 + len]; b[0] = buffer; System.arraycopy(userData, pos, b, 1, len); do { long r = next.write(b, 0, b.length); if (r == 0 && buffer.hasRemaining()) { return STATE_BUF_FLUSH; } } while (buffer.hasRemaining()); } bufferDone(); return STATE_BODY; } // not reached } // fall thru } // Clean-up states case STATE_HDR_EOL_CR: { if (!buffer.hasRemaining()) { if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) return STATE_HDR_EOL_CR; } buffer.put((byte) 13); // CR } case STATE_HDR_EOL_LF: { if (!buffer.hasRemaining()) { if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) return STATE_HDR_EOL_LF; } buffer.put((byte) 10); // LF if (valueIdx < headerValues.size()) { state = STATE_HDR_NAME; break; } else if ((fiCookie = headers.fiNextNonEmpty(fiCookie)) != -1L) { headerValues = headers.fiCurrent(fiCookie); valueIdx = 0; state = STATE_HDR_NAME; break; } // fall thru } case STATE_HDR_FINAL_CR: { if (!buffer.hasRemaining()) { if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) return STATE_HDR_FINAL_CR; } buffer.put((byte) 13); // CR // fall thru } case STATE_HDR_FINAL_LF: { if (!buffer.hasRemaining()) { if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) return STATE_HDR_FINAL_LF; } buffer.put((byte) 10); // LF this.fiCookie = -1L; this.valueIdx = 0; this.string = null; buffer.flip(); //for performance reasons we use a gather write if there is user data if (userData == null) { do { res = next.write(buffer); if (res == 0) { return STATE_BUF_FLUSH; } } while (buffer.hasRemaining()); } else if(userData instanceof ByteBuffer) { ByteBuffer[] b = {buffer, (ByteBuffer) userData}; do { long r = next.write(b, 0, b.length); if (r == 0 && buffer.hasRemaining()) { return STATE_BUF_FLUSH; } } while (buffer.hasRemaining()); } else { ByteBuffer[] b = new ByteBuffer[1 + len]; b[0] = buffer; System.arraycopy(userData, pos, b, 1, len); do { long r = next.write(b, 0, b.length); if (r == 0 && buffer.hasRemaining()) { return STATE_BUF_FLUSH; } } while (buffer.hasRemaining()); } // fall thru } case STATE_BUF_FLUSH: { // buffer was successfully flushed above bufferDone(); return STATE_BODY; } default: { throw new IllegalStateException(); } } } } private boolean flushHeaderBuffer(ByteBuffer buffer, String string, HeaderValues headerValues, int charIndex, long fiCookie, int valueIdx) throws IOException { int res; buffer.flip(); do { res = next.write(buffer); if (res == 0) { this.string = string; this.headerValues = headerValues; this.charIndex = charIndex; this.fiCookie = fiCookie; this.valueIdx = valueIdx; return true; } } while (buffer.hasRemaining()); buffer.clear(); return false; } public int write(final ByteBuffer src) throws IOException { try { int oldState = this.state; int state = oldState & MASK_STATE; int alreadyWritten = 0; int originalRemaining = -1; try { if (state != 0) { originalRemaining = src.remaining(); state = processWrite(state, src, -1, -1); if (state != 0) { return 0; } alreadyWritten = originalRemaining - src.remaining(); if (allAreSet(oldState, FLAG_SHUTDOWN)) { next.terminateWrites(); throw new ClosedChannelException(); } } if (alreadyWritten != originalRemaining) { return next.write(src) + alreadyWritten; } return alreadyWritten; } finally { this.state = oldState & ~MASK_STATE | state; } } catch(IOException|RuntimeException|Error e) { IoUtils.safeClose(connection); throw e; } } public long write(final ByteBuffer[] srcs) throws IOException { return write(srcs, 0, srcs.length); } public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { if (length == 0) { return 0L; } int oldVal = state; int state = oldVal & MASK_STATE; try { if (state != 0) { long rem = Buffers.remaining(srcs, offset, length); state = processWrite(state, srcs, offset, length); long ret = rem - Buffers.remaining(srcs, offset, length); if (state != 0) { return ret; } if (allAreSet(oldVal, FLAG_SHUTDOWN)) { next.terminateWrites(); throw new ClosedChannelException(); } //we don't attempt to write again return ret; } return length == 1 ? next.write(srcs[offset]) : next.write(srcs, offset, length); } catch (IOException | RuntimeException | Error e) { IoUtils.safeClose(connection); throw e; } finally { this.state = oldVal & ~MASK_STATE | state; } } public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { try { if (pooledFileTransferBuffer != null) { try { return write(pooledFileTransferBuffer.getBuffer()); } catch (IOException | RuntimeException | Error e) { if (pooledFileTransferBuffer != null) { pooledFileTransferBuffer.close(); pooledFileTransferBuffer = null; } throw e; } finally { if (pooledFileTransferBuffer != null) { if (!pooledFileTransferBuffer.getBuffer().hasRemaining()) { pooledFileTransferBuffer.close(); pooledFileTransferBuffer = null; } } } } else if (state != 0) { final PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().allocate(); ByteBuffer buffer = pooled.getBuffer(); try { int res = src.read(buffer); buffer.flip(); if (res <= 0) { return res; } return write(buffer); } finally { if (buffer.hasRemaining()) { pooledFileTransferBuffer = pooled; } else { pooled.close(); } } } else { return next.transferFrom(src, position, count); } } catch (IOException | RuntimeException | Error e) { IoUtils.safeClose(connection); throw e; } } public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { try { if (state != 0) { return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); } else { return next.transferFrom(source, count, throughBuffer); } } catch (IOException| RuntimeException | Error e) { IoUtils.safeClose(connection); throw e; } } @Override public int writeFinal(ByteBuffer src) throws IOException { try { return Conduits.writeFinalBasic(this, src); } catch (IOException | RuntimeException | Error e) { IoUtils.safeClose(connection); throw e; } } @Override public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { try { return Conduits.writeFinalBasic(this, srcs, offset, length); } catch (IOException | RuntimeException | Error e) { IoUtils.safeClose(connection); throw e; } } public boolean flush() throws IOException { int oldVal = state; int state = oldVal & MASK_STATE; try { if (state != 0) { state = processWrite(state, null, -1, -1); if (state != 0) { return false; } if (allAreSet(oldVal, FLAG_SHUTDOWN)) { next.terminateWrites(); // fall out to the flush } } return next.flush(); } catch (IOException | RuntimeException | Error e) { IoUtils.safeClose(connection); throw e; } finally { this.state = oldVal & ~MASK_STATE | state; } } public void terminateWrites() throws IOException { try { int oldVal = this.state; if (allAreClear(oldVal, MASK_STATE)) { next.terminateWrites(); return; } this.state = oldVal | FLAG_SHUTDOWN; } catch (IOException | RuntimeException | Error e) { IoUtils.safeClose(connection); throw e; } } public void truncateWrites() throws IOException { try { next.truncateWrites(); } catch (IOException | RuntimeException | Error e) { IoUtils.safeClose(connection); throw e; } finally { if (pooledBuffer != null) { bufferDone(); } if(pooledFileTransferBuffer != null) { pooledFileTransferBuffer.close(); pooledFileTransferBuffer = null; } } } public XnioWorker getWorker() { return next.getWorker(); } void freeBuffers() { done = true; if(pooledBuffer != null) { bufferDone(); } if(pooledFileTransferBuffer != null) { pooledFileTransferBuffer.close(); pooledFileTransferBuffer = null; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy