/* * Copyright 2013 The Netty Project * * The Netty Project 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.jboss.netty.handler.codec.spdy; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelDownstreamHandler; import org.jboss.netty.channel.ChannelEvent; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; import java.util.concurrent.atomic.AtomicInteger; import static org.jboss.netty.handler.codec.spdy.SpdyCodecUtil.*; /** * Manages streams within a SPDY session. */ public class SpdySessionHandler extends SimpleChannelUpstreamHandler implements ChannelDownstreamHandler { private static final SpdyProtocolException PROTOCOL_EXCEPTION = new SpdyProtocolException(); private static final int DEFAULT_WINDOW_SIZE = 64 * 1024; // 64 KB default initial window size private volatile int initialSendWindowSize = DEFAULT_WINDOW_SIZE; private volatile int initialReceiveWindowSize = DEFAULT_WINDOW_SIZE; private volatile int initialSessionReceiveWindowSize = DEFAULT_WINDOW_SIZE; private final SpdySession spdySession = new SpdySession(initialSendWindowSize, initialReceiveWindowSize); private volatile int lastGoodStreamId; private static final int DEFAULT_MAX_CONCURRENT_STREAMS = Integer.MAX_VALUE; private volatile int remoteConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS; private volatile int localConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS; private final Object flowControlLock = new Object(); private final AtomicInteger pings = new AtomicInteger(); private volatile boolean sentGoAwayFrame; private volatile boolean receivedGoAwayFrame; private volatile ChannelFutureListener closeSessionFutureListener; private final boolean server; private final int minorVersion; /** * Creates a new session handler. * * @param spdyVersion the protocol version * @param server {@code true} if and only if this session handler should * handle the server endpoint of the connection. * {@code false} if and only if this session handler should * handle the client endpoint of the connection. */ public SpdySessionHandler(SpdyVersion spdyVersion, boolean server) { if (spdyVersion == null) { throw new NullPointerException("spdyVersion"); } this.server = server; minorVersion = spdyVersion.getMinorVersion(); } public void setSessionReceiveWindowSize(int sessionReceiveWindowSize) { if (sessionReceiveWindowSize < 0) { throw new IllegalArgumentException("sessionReceiveWindowSize"); } // This will not send a window update frame immediately. // If this value increases the allowed receive window size, // a WINDOW_UPDATE frame will be sent when only half of the // session window size remains during data frame processing. // If this value decreases the allowed receive window size, // the window will be reduced as data frames are processed. initialSessionReceiveWindowSize = sessionReceiveWindowSize; } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { Object msg = e.getMessage(); if (msg instanceof SpdyDataFrame) { /* * SPDY Data frame processing requirements: * * If an endpoint receives a data frame for a Stream-ID which is not open * and the endpoint has not sent a GOAWAY frame, it must issue a stream error * with the error code INVALID_STREAM for the Stream-ID. * * If an endpoint which created the stream receives a data frame before receiving * a SYN_REPLY on that stream, it is a protocol error, and the recipient must * issue a stream error with the status code PROTOCOL_ERROR for the Stream-ID. * * If an endpoint receives multiple data frames for invalid Stream-IDs, * it may close the session. * * If an endpoint refuses a stream it must ignore any data frames for that stream. * * If an endpoint receives a data frame after the stream is half-closed from the * sender, it must send a RST_STREAM frame with the status STREAM_ALREADY_CLOSED. * * If an endpoint receives a data frame after the stream is closed, it must send * a RST_STREAM frame with the status PROTOCOL_ERROR. */ SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; int streamId = spdyDataFrame.getStreamId(); int deltaWindowSize = -1 * spdyDataFrame.getData().readableBytes(); int newSessionWindowSize = spdySession.updateReceiveWindowSize(SPDY_SESSION_STREAM_ID, deltaWindowSize); // Check if session window size is reduced beyond allowable lower bound if (newSessionWindowSize < 0) { issueSessionError(ctx, e.getChannel(), e.getRemoteAddress(), SpdySessionStatus.PROTOCOL_ERROR); return; } // Send a WINDOW_UPDATE frame if less than half the session window size remains if (newSessionWindowSize <= initialSessionReceiveWindowSize / 2) { int sessionDeltaWindowSize = initialSessionReceiveWindowSize - newSessionWindowSize; spdySession.updateReceiveWindowSize(SPDY_SESSION_STREAM_ID, sessionDeltaWindowSize); SpdyWindowUpdateFrame spdyWindowUpdateFrame = new DefaultSpdyWindowUpdateFrame(SPDY_SESSION_STREAM_ID, sessionDeltaWindowSize); Channels.write( ctx, Channels.future(e.getChannel()), spdyWindowUpdateFrame, e.getRemoteAddress()); } // Check if we received a data frame for a Stream-ID which is not open if (!spdySession.isActiveStream(streamId)) { if (streamId <= lastGoodStreamId) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.PROTOCOL_ERROR); } else if (!sentGoAwayFrame) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.INVALID_STREAM); } return; } // Check if we received a data frame for a stream which is half-closed if (spdySession.isRemoteSideClosed(streamId)) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.STREAM_ALREADY_CLOSED); return; } // Check if we received a data frame before receiving a SYN_REPLY if (!isRemoteInitiatedId(streamId) && !spdySession.hasReceivedReply(streamId)) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.PROTOCOL_ERROR); return; } /* * SPDY Data frame flow control processing requirements: * * Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame. */ // Update receive window size int newWindowSize = spdySession.updateReceiveWindowSize(streamId, deltaWindowSize); // Window size can become negative if we sent a SETTINGS frame that reduces the // size of the transfer window after the peer has written data frames. // The value is bounded by the length that SETTINGS frame decrease the window. // This difference is stored for the session when writing the SETTINGS frame // and is cleared once we send a WINDOW_UPDATE frame. if (newWindowSize < spdySession.getReceiveWindowSizeLowerBound(streamId)) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.FLOW_CONTROL_ERROR); return; } // Window size became negative due to sender writing frame before receiving SETTINGS // Send data frames upstream in initialReceiveWindowSize chunks if (newWindowSize < 0) { while (spdyDataFrame.getData().readableBytes() > initialReceiveWindowSize) { SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(streamId); partialDataFrame.setData(spdyDataFrame.getData().readSlice(initialReceiveWindowSize)); Channels.fireMessageReceived(ctx, partialDataFrame, e.getRemoteAddress()); } } // Send a WINDOW_UPDATE frame if less than half the stream window size remains if (newWindowSize <= initialReceiveWindowSize / 2 && !spdyDataFrame.isLast()) { int streamDeltaWindowSize = initialReceiveWindowSize - newWindowSize; spdySession.updateReceiveWindowSize(streamId, streamDeltaWindowSize); SpdyWindowUpdateFrame spdyWindowUpdateFrame = new DefaultSpdyWindowUpdateFrame(streamId, streamDeltaWindowSize); Channels.write( ctx, Channels.future(e.getChannel()), spdyWindowUpdateFrame, e.getRemoteAddress()); } // Close the remote side of the stream if this is the last frame if (spdyDataFrame.isLast()) { halfCloseStream(streamId, true, e.getFuture()); } } else if (msg instanceof SpdySynStreamFrame) { /* * SPDY SYN_STREAM frame processing requirements: * * If an endpoint receives a SYN_STREAM with a Stream-ID that is less than * any previously received SYN_STREAM, it must issue a session error with * the status PROTOCOL_ERROR. * * If an endpoint receives multiple SYN_STREAM frames with the same active * Stream-ID, it must issue a stream error with the status code PROTOCOL_ERROR. * * The recipient can reject a stream by sending a stream error with the * status code REFUSED_STREAM. */ SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; int streamId = spdySynStreamFrame.getStreamId(); // Check if we received a valid SYN_STREAM frame if (spdySynStreamFrame.isInvalid() || !isRemoteInitiatedId(streamId) || spdySession.isActiveStream(streamId)) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.PROTOCOL_ERROR); return; } // Stream-IDs must be monotonically increasing if (streamId <= lastGoodStreamId) { issueSessionError(ctx, e.getChannel(), e.getRemoteAddress(), SpdySessionStatus.PROTOCOL_ERROR); return; } // Try to accept the stream byte priority = spdySynStreamFrame.getPriority(); boolean remoteSideClosed = spdySynStreamFrame.isLast(); boolean localSideClosed = spdySynStreamFrame.isUnidirectional(); if (!acceptStream(streamId, priority, remoteSideClosed, localSideClosed)) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.REFUSED_STREAM); return; } } else if (msg instanceof SpdySynReplyFrame) { /* * SPDY SYN_REPLY frame processing requirements: * * If an endpoint receives multiple SYN_REPLY frames for the same active Stream-ID * it must issue a stream error with the status code STREAM_IN_USE. */ SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; int streamId = spdySynReplyFrame.getStreamId(); // Check if we received a valid SYN_REPLY frame if (spdySynReplyFrame.isInvalid() || isRemoteInitiatedId(streamId) || spdySession.isRemoteSideClosed(streamId)) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.INVALID_STREAM); return; } // Check if we have received multiple frames for the same Stream-ID if (spdySession.hasReceivedReply(streamId)) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.STREAM_IN_USE); return; } spdySession.receivedReply(streamId); // Close the remote side of the stream if this is the last frame if (spdySynReplyFrame.isLast()) { halfCloseStream(streamId, true, e.getFuture()); } } else if (msg instanceof SpdyRstStreamFrame) { /* * SPDY RST_STREAM frame processing requirements: * * After receiving a RST_STREAM on a stream, the receiver must not send * additional frames on that stream. * * An endpoint must not send a RST_STREAM in response to a RST_STREAM. */ SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; removeStream(spdyRstStreamFrame.getStreamId(), e.getFuture()); } else if (msg instanceof SpdySettingsFrame) { SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; int settingsMinorVersion = spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MINOR_VERSION); if (settingsMinorVersion >= 0 && settingsMinorVersion != minorVersion) { // Settings frame had the wrong minor version issueSessionError(ctx, e.getChannel(), e.getRemoteAddress(), SpdySessionStatus.PROTOCOL_ERROR); return; } int newConcurrentStreams = spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS); if (newConcurrentStreams >= 0) { remoteConcurrentStreams = newConcurrentStreams; } // Persistence flag are inconsistent with the use of SETTINGS to communicate // the initial window size. Remove flags from the sender requesting that the // value be persisted. Remove values that the sender indicates are persisted. if (spdySettingsFrame.isPersisted(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE)) { spdySettingsFrame.removeValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); } spdySettingsFrame.setPersistValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE, false); int newInitialWindowSize = spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); if (newInitialWindowSize >= 0) { updateInitialSendWindowSize(newInitialWindowSize); } } else if (msg instanceof SpdyPingFrame) { /* * SPDY PING frame processing requirements: * * Receivers of a PING frame should send an identical frame to the sender * as soon as possible. * * Receivers of a PING frame must ignore frames that it did not initiate */ SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; if (isRemoteInitiatedId(spdyPingFrame.getId())) { Channels.write(ctx, Channels.future(e.getChannel()), spdyPingFrame, e.getRemoteAddress()); return; } // Note: only checks that there are outstanding pings since uniqueness is not enforced if (pings.get() == 0) { return; } pings.getAndDecrement(); } else if (msg instanceof SpdyGoAwayFrame) { receivedGoAwayFrame = true; } else if (msg instanceof SpdyHeadersFrame) { SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; int streamId = spdyHeadersFrame.getStreamId(); // Check if we received a valid HEADERS frame if (spdyHeadersFrame.isInvalid()) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.PROTOCOL_ERROR); return; } if (spdySession.isRemoteSideClosed(streamId)) { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.INVALID_STREAM); return; } // Close the remote side of the stream if this is the last frame if (spdyHeadersFrame.isLast()) { halfCloseStream(streamId, true, e.getFuture()); } } else if (msg instanceof SpdyWindowUpdateFrame) { /* * SPDY WINDOW_UPDATE frame processing requirements: * * Receivers of a WINDOW_UPDATE that cause the window size to exceed 2^31 * must send a RST_STREAM with the status code FLOW_CONTROL_ERROR. * * Sender should ignore all WINDOW_UPDATE frames associated with a stream * after sending the last frame for the stream. */ SpdyWindowUpdateFrame spdyWindowUpdateFrame = (SpdyWindowUpdateFrame) msg; int streamId = spdyWindowUpdateFrame.getStreamId(); int deltaWindowSize = spdyWindowUpdateFrame.getDeltaWindowSize(); // Ignore frames for half-closed streams if (streamId != SPDY_SESSION_STREAM_ID && spdySession.isLocalSideClosed(streamId)) { return; } // Check for numerical overflow if (spdySession.getSendWindowSize(streamId) > Integer.MAX_VALUE - deltaWindowSize) { if (streamId == SPDY_SESSION_STREAM_ID) { issueSessionError(ctx, e.getChannel(), e.getRemoteAddress(), SpdySessionStatus.PROTOCOL_ERROR); } else { issueStreamError(ctx, e.getRemoteAddress(), streamId, SpdyStreamStatus.FLOW_CONTROL_ERROR); } return; } updateSendWindowSize(ctx, streamId, deltaWindowSize); return; } super.messageReceived(ctx, e); } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { Throwable cause = e.getCause(); if (cause instanceof SpdyProtocolException) { issueSessionError(ctx, e.getChannel(), null, SpdySessionStatus.PROTOCOL_ERROR); } super.exceptionCaught(ctx, e); } public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent evt) throws Exception { if (evt instanceof ChannelStateEvent) { ChannelStateEvent e = (ChannelStateEvent) evt; switch (e.getState()) { case OPEN: case CONNECTED: case BOUND: /* * SPDY connection requirements: * * When either endpoint closes the transport-level connection, * it must first send a GOAWAY frame. */ if (Boolean.FALSE.equals(e.getValue()) || e.getValue() == null) { sendGoAwayFrame(ctx, e); return; } } } if (!(evt instanceof MessageEvent)) { ctx.sendDownstream(evt); return; } MessageEvent e = (MessageEvent) evt; Object msg = e.getMessage(); if (msg instanceof SpdyDataFrame) { SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; final int streamId = spdyDataFrame.getStreamId(); // Frames must not be sent on half-closed streams if (spdySession.isLocalSideClosed(streamId)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } /* * SPDY Data frame flow control processing requirements: * * Sender must not send a data frame with data length greater * than the transfer window size. * * After sending each data frame, the sender decrements its * transfer window size by the amount of data transmitted. * * When the window size becomes less than or equal to 0, the * sender must pause transmitting data frames. */ synchronized (flowControlLock) { int dataLength = spdyDataFrame.getData().readableBytes(); int sendWindowSize = spdySession.getSendWindowSize(streamId); int sessionSendWindowSize = spdySession.getSendWindowSize(SPDY_SESSION_STREAM_ID); sendWindowSize = Math.min(sendWindowSize, sessionSendWindowSize); if (sendWindowSize <= 0) { // Stream is stalled -- enqueue Data frame and return spdySession.putPendingWrite(streamId, e); return; } else if (sendWindowSize < dataLength) { // Stream is not stalled but we cannot send the entire frame spdySession.updateSendWindowSize(streamId, -1 * sendWindowSize); spdySession.updateSendWindowSize(SPDY_SESSION_STREAM_ID, -1 * sendWindowSize); // Create a partial data frame whose length is the current window size SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(streamId); partialDataFrame.setData(spdyDataFrame.getData().readSlice(sendWindowSize)); // Enqueue the remaining data (will be the first frame queued) spdySession.putPendingWrite(streamId, e); ChannelFuture writeFuture = Channels.future(e.getChannel()); // The transfer window size is pre-decremented when sending a data frame downstream. // Close the session on write failures that leaves the transfer window in a corrupt state. final SocketAddress remoteAddress = e.getRemoteAddress(); final ChannelHandlerContext context = ctx; e.getFuture().addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { Channel channel = future.getChannel(); issueSessionError( context, channel, remoteAddress, SpdySessionStatus.INTERNAL_ERROR); } } }); Channels.write(ctx, writeFuture, partialDataFrame, remoteAddress); return; } else { // Window size is large enough to send entire data frame spdySession.updateSendWindowSize(streamId, -1 * dataLength); spdySession.updateSendWindowSize(SPDY_SESSION_STREAM_ID, -1 * dataLength); // The transfer window size is pre-decremented when sending a data frame downstream. // Close the session on write failures that leaves the transfer window in a corrupt state. final SocketAddress remoteAddress = e.getRemoteAddress(); final ChannelHandlerContext context = ctx; e.getFuture().addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { Channel channel = future.getChannel(); issueSessionError( context, channel, remoteAddress, SpdySessionStatus.INTERNAL_ERROR); } } }); } } // Close the local side of the stream if this is the last frame if (spdyDataFrame.isLast()) { halfCloseStream(streamId, false, e.getFuture()); } } else if (msg instanceof SpdySynStreamFrame) { SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; int streamId = spdySynStreamFrame.getStreamId(); if (isRemoteInitiatedId(streamId)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } byte priority = spdySynStreamFrame.getPriority(); boolean remoteSideClosed = spdySynStreamFrame.isUnidirectional(); boolean localSideClosed = spdySynStreamFrame.isLast(); if (!acceptStream(streamId, priority, remoteSideClosed, localSideClosed)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } } else if (msg instanceof SpdySynReplyFrame) { SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; int streamId = spdySynReplyFrame.getStreamId(); // Frames must not be sent on half-closed streams if (!isRemoteInitiatedId(streamId) || spdySession.isLocalSideClosed(streamId)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } // Close the local side of the stream if this is the last frame if (spdySynReplyFrame.isLast()) { halfCloseStream(streamId, false, e.getFuture()); } } else if (msg instanceof SpdyRstStreamFrame) { SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; removeStream(spdyRstStreamFrame.getStreamId(), e.getFuture()); } else if (msg instanceof SpdySettingsFrame) { SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; int settingsMinorVersion = spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MINOR_VERSION); if (settingsMinorVersion >= 0 && settingsMinorVersion != minorVersion) { // Settings frame had the wrong minor version e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } int newConcurrentStreams = spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS); if (newConcurrentStreams >= 0) { localConcurrentStreams = newConcurrentStreams; } // Persistence flag are inconsistent with the use of SETTINGS to communicate // the initial window size. Remove flags from the sender requesting that the // value be persisted. Remove values that the sender indicates are persisted. if (spdySettingsFrame.isPersisted(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE)) { spdySettingsFrame.removeValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); } spdySettingsFrame.setPersistValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE, false); int newInitialWindowSize = spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); if (newInitialWindowSize >= 0) { updateInitialReceiveWindowSize(newInitialWindowSize); } } else if (msg instanceof SpdyPingFrame) { SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; if (isRemoteInitiatedId(spdyPingFrame.getId())) { e.getFuture().setFailure(new IllegalArgumentException( "invalid PING ID: " + spdyPingFrame.getId())); return; } pings.getAndIncrement(); } else if (msg instanceof SpdyGoAwayFrame) { // Why is this being sent? Intercept it and fail the write. // Should have sent a CLOSE ChannelStateEvent e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } else if (msg instanceof SpdyHeadersFrame) { SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; int streamId = spdyHeadersFrame.getStreamId(); // Frames must not be sent on half-closed streams if (spdySession.isLocalSideClosed(streamId)) { e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } // Close the local side of the stream if this is the last frame if (spdyHeadersFrame.isLast()) { halfCloseStream(streamId, false, e.getFuture()); } } else if (msg instanceof SpdyWindowUpdateFrame) { // Why is this being sent? Intercept it and fail the write. e.getFuture().setFailure(PROTOCOL_EXCEPTION); return; } ctx.sendDownstream(evt); } /* * SPDY Session Error Handling: * * When a session error occurs, the endpoint encountering the error must first * send a GOAWAY frame with the Stream-ID of the most recently received stream * from the remote endpoint, and the error code for why the session is terminating. * * After sending the GOAWAY frame, the endpoint must close the TCP connection. */ private void issueSessionError( ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress, SpdySessionStatus status) { ChannelFuture future = sendGoAwayFrame(ctx, channel, remoteAddress, status); future.addListener(ChannelFutureListener.CLOSE); } /* * SPDY Stream Error Handling: * * Upon a stream error, the endpoint must send a RST_STREAM frame which contains * the Stream-ID for the stream where the error occurred and the error status which * caused the error. * * After sending the RST_STREAM, the stream is closed to the sending endpoint. * * Note: this is only called by the worker thread */ private void issueStreamError( ChannelHandlerContext ctx, SocketAddress remoteAddress, int streamId, SpdyStreamStatus status) { boolean fireMessageReceived = !spdySession.isRemoteSideClosed(streamId); ChannelFuture future = Channels.future(ctx.getChannel()); removeStream(streamId, future); SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamId, status); Channels.write(ctx, future, spdyRstStreamFrame, remoteAddress); if (fireMessageReceived) { Channels.fireMessageReceived(ctx, spdyRstStreamFrame, remoteAddress); } } /* * Helper functions */ private boolean isRemoteInitiatedId(int id) { boolean serverId = isServerId(id); return server && !serverId || !server && serverId; } // need to synchronize to prevent new streams from being created while updating active streams private synchronized void updateInitialSendWindowSize(int newInitialWindowSize) { int deltaWindowSize = newInitialWindowSize - initialSendWindowSize; initialSendWindowSize = newInitialWindowSize; spdySession.updateAllSendWindowSizes(deltaWindowSize); } // need to synchronize to prevent new streams from being created while updating active streams private synchronized void updateInitialReceiveWindowSize(int newInitialWindowSize) { int deltaWindowSize = newInitialWindowSize - initialReceiveWindowSize; initialReceiveWindowSize = newInitialWindowSize; spdySession.updateAllReceiveWindowSizes(deltaWindowSize); } // need to synchronize accesses to sentGoAwayFrame, lastGoodStreamId, and initial window sizes private synchronized boolean acceptStream( int streamId, byte priority, boolean remoteSideClosed, boolean localSideClosed) { // Cannot initiate any new streams after receiving or sending GOAWAY if (receivedGoAwayFrame || sentGoAwayFrame) { return false; } boolean remote = isRemoteInitiatedId(streamId); int maxConcurrentStreams = remote ? localConcurrentStreams : remoteConcurrentStreams; if (spdySession.numActiveStreams(remote) >= maxConcurrentStreams) { return false; } spdySession.acceptStream( streamId, priority, remoteSideClosed, localSideClosed, initialSendWindowSize, initialReceiveWindowSize, remote); if (remote) { lastGoodStreamId = streamId; } return true; } private void halfCloseStream(int streamId, boolean remote, ChannelFuture future) { if (remote) { spdySession.closeRemoteSide(streamId, isRemoteInitiatedId(streamId)); } else { spdySession.closeLocalSide(streamId, isRemoteInitiatedId(streamId)); } if (closeSessionFutureListener != null && spdySession.noActiveStreams()) { future.addListener(closeSessionFutureListener); } } private void removeStream(int streamId, ChannelFuture future) { spdySession.removeStream(streamId, isRemoteInitiatedId(streamId)); if (closeSessionFutureListener != null && spdySession.noActiveStreams()) { future.addListener(closeSessionFutureListener); } } private void updateSendWindowSize(ChannelHandlerContext ctx, int streamId, int deltaWindowSize) { synchronized (flowControlLock) { int newWindowSize = spdySession.updateSendWindowSize(streamId, deltaWindowSize); if (streamId != SPDY_SESSION_STREAM_ID) { int sessionSendWindowSize = spdySession.getSendWindowSize(SPDY_SESSION_STREAM_ID); newWindowSize = Math.min(newWindowSize, sessionSendWindowSize); } while (newWindowSize > 0) { // Check if we have unblocked a stalled stream MessageEvent e = spdySession.getPendingWrite(streamId); if (e == null) { break; } SpdyDataFrame spdyDataFrame = (SpdyDataFrame) e.getMessage(); int dataFrameSize = spdyDataFrame.getData().readableBytes(); int writeStreamId = spdyDataFrame.getStreamId(); if (streamId == SPDY_SESSION_STREAM_ID) { newWindowSize = Math.min(newWindowSize, spdySession.getSendWindowSize(writeStreamId)); } if (newWindowSize >= dataFrameSize) { // Window size is large enough to send entire data frame spdySession.removePendingWrite(writeStreamId); newWindowSize = spdySession.updateSendWindowSize(writeStreamId, -1 * dataFrameSize); int sessionSendWindowSize = spdySession.updateSendWindowSize(SPDY_SESSION_STREAM_ID, -1 * dataFrameSize); newWindowSize = Math.min(newWindowSize, sessionSendWindowSize); // The transfer window size is pre-decremented when sending a data frame downstream. // Close the session on write failures that leaves the transfer window in a corrupt state. final SocketAddress remoteAddress = e.getRemoteAddress(); final ChannelHandlerContext context = ctx; e.getFuture().addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { Channel channel = future.getChannel(); issueSessionError(context, channel, remoteAddress, SpdySessionStatus.INTERNAL_ERROR); } } }); // Close the local side of the stream if this is the last frame if (spdyDataFrame.isLast()) { halfCloseStream(writeStreamId, false, e.getFuture()); } Channels.write(ctx, e.getFuture(), spdyDataFrame, e.getRemoteAddress()); } else { // We can send a partial frame spdySession.updateSendWindowSize(writeStreamId, -1 * newWindowSize); spdySession.updateSendWindowSize(SPDY_SESSION_STREAM_ID, -1 * newWindowSize); // Create a partial data frame whose length is the current window size SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(writeStreamId); partialDataFrame.setData(spdyDataFrame.getData().readSlice(newWindowSize)); ChannelFuture writeFuture = Channels.future(e.getChannel()); // The transfer window size is pre-decremented when sending a data frame downstream. // Close the session on write failures that leaves the transfer window in a corrupt state. final SocketAddress remoteAddress = e.getRemoteAddress(); final ChannelHandlerContext context = ctx; e.getFuture().addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { Channel channel = future.getChannel(); issueSessionError(context, channel, remoteAddress, SpdySessionStatus.INTERNAL_ERROR); } } }); Channels.write(ctx, writeFuture, partialDataFrame, remoteAddress); newWindowSize = 0; } } } } private void sendGoAwayFrame(ChannelHandlerContext ctx, ChannelStateEvent e) { // Avoid NotYetConnectedException if (!e.getChannel().isConnected()) { ctx.sendDownstream(e); return; } ChannelFuture future = sendGoAwayFrame(ctx, e.getChannel(), null, SpdySessionStatus.OK); if (spdySession.noActiveStreams()) { future.addListener(new ClosingChannelFutureListener(ctx, e)); } else { closeSessionFutureListener = new ClosingChannelFutureListener(ctx, e); } } private synchronized ChannelFuture sendGoAwayFrame( ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress, SpdySessionStatus status) { if (!sentGoAwayFrame) { sentGoAwayFrame = true; SpdyGoAwayFrame spdyGoAwayFrame = new DefaultSpdyGoAwayFrame(lastGoodStreamId, status); ChannelFuture future = Channels.future(channel); Channels.write(ctx, future, spdyGoAwayFrame, remoteAddress); return future; } return Channels.succeededFuture(channel); } private static final class ClosingChannelFutureListener implements ChannelFutureListener { private final ChannelHandlerContext ctx; private final ChannelStateEvent e; ClosingChannelFutureListener(ChannelHandlerContext ctx, ChannelStateEvent e) { this.ctx = ctx; this.e = e; } public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception { if (!(sentGoAwayFuture.getCause() instanceof ClosedChannelException)) { Channels.close(ctx, e.getFuture()); } else { e.getFuture().setSuccess(); } } } }