View Javadoc
1   /*
2    * Copyright 2013 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.spdy;
17  
18  import io.netty.channel.ChannelDuplexHandler;
19  import io.netty.channel.ChannelFuture;
20  import io.netty.channel.ChannelFutureListener;
21  import io.netty.channel.ChannelHandlerContext;
22  import io.netty.channel.ChannelPromise;
23  import io.netty.util.internal.ObjectUtil;
24  
25  import java.util.concurrent.atomic.AtomicInteger;
26  
27  import static io.netty.handler.codec.spdy.SpdyCodecUtil.SPDY_SESSION_STREAM_ID;
28  import static io.netty.handler.codec.spdy.SpdyCodecUtil.isServerId;
29  import static io.netty.util.internal.ObjectUtil.checkPositive;
30  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
31  
32  /**
33   * Manages streams within a SPDY session.
34   */
35  public class SpdySessionHandler extends ChannelDuplexHandler {
36  
37      private static final SpdyProtocolException PROTOCOL_EXCEPTION =
38              SpdyProtocolException.newStatic(null, SpdySessionHandler.class, "handleOutboundMessage(...)");
39      private static final SpdyProtocolException STREAM_CLOSED =
40              SpdyProtocolException.newStatic("Stream closed", SpdySessionHandler.class, "removeStream(...)");
41  
42      private static final int DEFAULT_WINDOW_SIZE = 64 * 1024; // 64 KB default initial window size
43      private int initialSendWindowSize    = DEFAULT_WINDOW_SIZE;
44      private int initialReceiveWindowSize = DEFAULT_WINDOW_SIZE;
45      private volatile int initialSessionReceiveWindowSize = DEFAULT_WINDOW_SIZE;
46  
47      private final SpdySession spdySession = new SpdySession(initialSendWindowSize, initialReceiveWindowSize);
48      private int lastGoodStreamId;
49  
50      // See https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2 for why 100 was chosen as a safe default.
51      private static final int DEFAULT_MAX_CONCURRENT_STREAMS = 100;
52      private int remoteConcurrentStreams = Integer.MAX_VALUE;
53      private int localConcurrentStreams  = DEFAULT_MAX_CONCURRENT_STREAMS;
54  
55      private final AtomicInteger pings = new AtomicInteger();
56  
57      private boolean sentGoAwayFrame;
58      private boolean receivedGoAwayFrame;
59      private boolean sentInitialSettingsFrame;
60  
61      private ChannelFutureListener closeSessionFutureListener;
62  
63      private final boolean server;
64      private final int minorVersion;
65  
66      /**
67       * Creates a new session handler.
68       * <p>
69       * Remote-initiated streams are accepted up to {@value #DEFAULT_MAX_CONCURRENT_STREAMS}
70       * concurrently (matching the HTTP/2 default, see
71       * <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">RFC 7540, Section 6.5.2</a>).
72       * Use {@link #SpdySessionHandler(SpdyVersion, boolean, int)} to configure a different limit.
73       *
74       * @param version the protocol version
75       * @param server  {@code true} if and only if this session handler should
76       *                handle the server endpoint of the connection.
77       *                {@code false} if and only if this session handler should
78       *                handle the client endpoint of the connection.
79       */
80      public SpdySessionHandler(SpdyVersion version, boolean server) {
81          this(version, server, DEFAULT_MAX_CONCURRENT_STREAMS);
82      }
83  
84      /**
85       * Creates a new session handler.
86       *
87       * @param version the protocol version
88       * @param server  {@code true} if and only if this session handler should
89       *                handle the server endpoint of the connection.
90       *                {@code false} if and only if this session handler should
91       *                handle the client endpoint of the connection.
92       * @param maxLocalConcurrentStreams the maximum number of concurrent remote-initiated streams
93       *                that will be accepted. This bounds the memory a remote peer can force this
94       *                handler to allocate by opening streams. {@code SYN_STREAM} frames received
95       *                once this limit is reached are rejected with a {@code RST_STREAM} carrying
96       *                {@link SpdyStreamStatus#REFUSED_STREAM}. Must be positive.
97       */
98      public SpdySessionHandler(SpdyVersion version, boolean server, int maxLocalConcurrentStreams) {
99          this.minorVersion = ObjectUtil.checkNotNull(version, "version").minorVersion();
100         this.server = server;
101         this.localConcurrentStreams = checkPositive(maxLocalConcurrentStreams, "maxLocalConcurrentStreams");
102     }
103 
104     public void setSessionReceiveWindowSize(int sessionReceiveWindowSize) {
105         checkPositiveOrZero(sessionReceiveWindowSize, "sessionReceiveWindowSize");
106         // This will not send a window update frame immediately.
107         // If this value increases the allowed receive window size,
108         // a WINDOW_UPDATE frame will be sent when only half of the
109         // session window size remains during data frame processing.
110         // If this value decreases the allowed receive window size,
111         // the window will be reduced as data frames are processed.
112         initialSessionReceiveWindowSize = sessionReceiveWindowSize;
113     }
114 
115     @Override
116     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
117         if (ctx.channel().isActive()) {
118             sendInitialSettingsFrame(ctx);
119         }
120     }
121 
122     @Override
123     public void channelActive(ChannelHandlerContext ctx) throws Exception {
124         sendInitialSettingsFrame(ctx);
125         super.channelActive(ctx);
126     }
127 
128     private void sendInitialSettingsFrame(ChannelHandlerContext ctx) {
129         if (sentInitialSettingsFrame) {
130             return;
131         }
132         sentInitialSettingsFrame = true;
133         // Advertise the accept limit to the remote peer. This is advisory only -- the limit is
134         // enforced locally by acceptStream(...) regardless of whether the peer honors it.
135         SpdySettingsFrame settingsFrame = new DefaultSpdySettingsFrame();
136         settingsFrame.setValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS, localConcurrentStreams);
137         ctx.writeAndFlush(settingsFrame);
138     }
139 
140     @Override
141     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
142         if (msg instanceof SpdyDataFrame) {
143 
144             /*
145              * SPDY Data frame processing requirements:
146              *
147              * If an endpoint receives a data frame for a Stream-ID which is not open
148              * and the endpoint has not sent a GOAWAY frame, it must issue a stream error
149              * with the error code INVALID_STREAM for the Stream-ID.
150              *
151              * If an endpoint which created the stream receives a data frame before receiving
152              * a SYN_REPLY on that stream, it is a protocol error, and the recipient must
153              * issue a stream error with the getStatus code PROTOCOL_ERROR for the Stream-ID.
154              *
155              * If an endpoint receives multiple data frames for invalid Stream-IDs,
156              * it may close the session.
157              *
158              * If an endpoint refuses a stream it must ignore any data frames for that stream.
159              *
160              * If an endpoint receives a data frame after the stream is half-closed from the
161              * sender, it must send a RST_STREAM frame with the getStatus STREAM_ALREADY_CLOSED.
162              *
163              * If an endpoint receives a data frame after the stream is closed, it must send
164              * a RST_STREAM frame with the getStatus PROTOCOL_ERROR.
165              */
166             SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
167             int streamId = spdyDataFrame.streamId();
168 
169             int deltaWindowSize = -1 * spdyDataFrame.content().readableBytes();
170             int newSessionWindowSize =
171                 spdySession.updateReceiveWindowSize(SPDY_SESSION_STREAM_ID, deltaWindowSize);
172 
173             // Check if session window size is reduced beyond allowable lower bound
174             if (newSessionWindowSize < 0) {
175                 spdyDataFrame.release();
176                 issueSessionError(ctx, SpdySessionStatus.PROTOCOL_ERROR);
177                 return;
178             }
179 
180             // Send a WINDOW_UPDATE frame if less than half the session window size remains
181             if (newSessionWindowSize <= initialSessionReceiveWindowSize / 2) {
182                 int sessionDeltaWindowSize = initialSessionReceiveWindowSize - newSessionWindowSize;
183                 spdySession.updateReceiveWindowSize(SPDY_SESSION_STREAM_ID, sessionDeltaWindowSize);
184                 SpdyWindowUpdateFrame spdyWindowUpdateFrame =
185                     new DefaultSpdyWindowUpdateFrame(SPDY_SESSION_STREAM_ID, sessionDeltaWindowSize);
186                 ctx.writeAndFlush(spdyWindowUpdateFrame);
187             }
188 
189             // Check if we received a data frame for a Stream-ID which is not open
190 
191             if (!spdySession.isActiveStream(streamId)) {
192                 spdyDataFrame.release();
193                 if (streamId <= lastGoodStreamId) {
194                     issueStreamError(ctx, streamId, SpdyStreamStatus.PROTOCOL_ERROR);
195                 } else if (!sentGoAwayFrame) {
196                     issueStreamError(ctx, streamId, SpdyStreamStatus.INVALID_STREAM);
197                 }
198                 return;
199             }
200 
201             // Check if we received a data frame for a stream which is half-closed
202 
203             if (spdySession.isRemoteSideClosed(streamId)) {
204                 spdyDataFrame.release();
205                 issueStreamError(ctx, streamId, SpdyStreamStatus.STREAM_ALREADY_CLOSED);
206                 return;
207             }
208 
209             // Check if we received a data frame before receiving a SYN_REPLY
210             if (!isRemoteInitiatedId(streamId) && !spdySession.hasReceivedReply(streamId)) {
211                 spdyDataFrame.release();
212                 issueStreamError(ctx, streamId, SpdyStreamStatus.PROTOCOL_ERROR);
213                 return;
214             }
215 
216             /*
217              * SPDY Data frame flow control processing requirements:
218              *
219              * Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame.
220              */
221 
222             // Update receive window size
223             int newWindowSize = spdySession.updateReceiveWindowSize(streamId, deltaWindowSize);
224 
225             // Window size can become negative if we sent a SETTINGS frame that reduces the
226             // size of the transfer window after the peer has written data frames.
227             // The value is bounded by the length that SETTINGS frame decrease the window.
228             // This difference is stored for the session when writing the SETTINGS frame
229             // and is cleared once we send a WINDOW_UPDATE frame.
230             if (newWindowSize < spdySession.getReceiveWindowSizeLowerBound(streamId)) {
231                 spdyDataFrame.release();
232                 issueStreamError(ctx, streamId, SpdyStreamStatus.FLOW_CONTROL_ERROR);
233                 return;
234             }
235 
236             // Window size became negative due to sender writing frame before receiving SETTINGS
237             // Send data frames upstream in initialReceiveWindowSize chunks
238             if (newWindowSize < 0) {
239                 while (spdyDataFrame.content().readableBytes() > initialReceiveWindowSize) {
240                     SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(
241                             streamId, spdyDataFrame.content().readRetainedSlice(initialReceiveWindowSize));
242                     ctx.writeAndFlush(partialDataFrame);
243                 }
244             }
245 
246             // Send a WINDOW_UPDATE frame if less than half the stream window size remains
247             if (newWindowSize <= initialReceiveWindowSize / 2 && !spdyDataFrame.isLast()) {
248                 int streamDeltaWindowSize = initialReceiveWindowSize - newWindowSize;
249                 spdySession.updateReceiveWindowSize(streamId, streamDeltaWindowSize);
250                 SpdyWindowUpdateFrame spdyWindowUpdateFrame =
251                         new DefaultSpdyWindowUpdateFrame(streamId, streamDeltaWindowSize);
252                 ctx.writeAndFlush(spdyWindowUpdateFrame);
253             }
254 
255             // Close the remote side of the stream if this is the last frame
256             if (spdyDataFrame.isLast()) {
257                 halfCloseStream(streamId, true, ctx.newSucceededFuture());
258             }
259 
260         } else if (msg instanceof SpdySynStreamFrame) {
261 
262             /*
263              * SPDY SYN_STREAM frame processing requirements:
264              *
265              * If an endpoint receives a SYN_STREAM with a Stream-ID that is less than
266              * any previously received SYN_STREAM, it must issue a session error with
267              * the getStatus PROTOCOL_ERROR.
268              *
269              * If an endpoint receives multiple SYN_STREAM frames with the same active
270              * Stream-ID, it must issue a stream error with the getStatus code PROTOCOL_ERROR.
271              *
272              * The recipient can reject a stream by sending a stream error with the
273              * getStatus code REFUSED_STREAM.
274              */
275 
276             SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg;
277             int streamId = spdySynStreamFrame.streamId();
278 
279             // Check if we received a valid SYN_STREAM frame
280             if (spdySynStreamFrame.isInvalid() ||
281                 !isRemoteInitiatedId(streamId) ||
282                 spdySession.isActiveStream(streamId)) {
283                 issueStreamError(ctx, streamId, SpdyStreamStatus.PROTOCOL_ERROR);
284                 return;
285             }
286 
287             // Stream-IDs must be monotonically increasing
288             if (streamId <= lastGoodStreamId) {
289                 issueSessionError(ctx, SpdySessionStatus.PROTOCOL_ERROR);
290                 return;
291             }
292 
293             // Try to accept the stream
294             byte priority = spdySynStreamFrame.priority();
295             boolean remoteSideClosed = spdySynStreamFrame.isLast();
296             boolean localSideClosed = spdySynStreamFrame.isUnidirectional();
297             if (!acceptStream(streamId, priority, remoteSideClosed, localSideClosed)) {
298                 issueStreamError(ctx, streamId, SpdyStreamStatus.REFUSED_STREAM);
299                 return;
300             }
301 
302         } else if (msg instanceof SpdySynReplyFrame) {
303 
304             /*
305              * SPDY SYN_REPLY frame processing requirements:
306              *
307              * If an endpoint receives multiple SYN_REPLY frames for the same active Stream-ID
308              * it must issue a stream error with the getStatus code STREAM_IN_USE.
309              */
310 
311             SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
312             int streamId = spdySynReplyFrame.streamId();
313 
314             // Check if we received a valid SYN_REPLY frame
315             if (spdySynReplyFrame.isInvalid() ||
316                 isRemoteInitiatedId(streamId) ||
317                 spdySession.isRemoteSideClosed(streamId)) {
318                 issueStreamError(ctx, streamId, SpdyStreamStatus.INVALID_STREAM);
319                 return;
320             }
321 
322             // Check if we have received multiple frames for the same Stream-ID
323             if (spdySession.hasReceivedReply(streamId)) {
324                 issueStreamError(ctx, streamId, SpdyStreamStatus.STREAM_IN_USE);
325                 return;
326             }
327 
328             spdySession.receivedReply(streamId);
329 
330             // Close the remote side of the stream if this is the last frame
331             if (spdySynReplyFrame.isLast()) {
332                 halfCloseStream(streamId, true, ctx.newSucceededFuture());
333             }
334 
335         } else if (msg instanceof SpdyRstStreamFrame) {
336 
337             /*
338              * SPDY RST_STREAM frame processing requirements:
339              *
340              * After receiving a RST_STREAM on a stream, the receiver must not send
341              * additional frames on that stream.
342              *
343              * An endpoint must not send a RST_STREAM in response to a RST_STREAM.
344              */
345 
346             SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
347             removeStream(spdyRstStreamFrame.streamId(), ctx.newSucceededFuture());
348 
349         } else if (msg instanceof SpdySettingsFrame) {
350 
351             SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg;
352 
353             int settingsMinorVersion = spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MINOR_VERSION);
354             if (settingsMinorVersion >= 0 && settingsMinorVersion != minorVersion) {
355                 // Settings frame had the wrong minor version
356                 issueSessionError(ctx, SpdySessionStatus.PROTOCOL_ERROR);
357                 return;
358             }
359 
360             int newConcurrentStreams =
361                 spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS);
362             if (newConcurrentStreams >= 0) {
363                 remoteConcurrentStreams = newConcurrentStreams;
364             }
365 
366             // Persistence flag are inconsistent with the use of SETTINGS to communicate
367             // the initial window size. Remove flags from the sender requesting that the
368             // value be persisted. Remove values that the sender indicates are persisted.
369             if (spdySettingsFrame.isPersisted(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE)) {
370                 spdySettingsFrame.removeValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE);
371             }
372             spdySettingsFrame.setPersistValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE, false);
373 
374             int newInitialWindowSize =
375                 spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE);
376             if (newInitialWindowSize >= 0) {
377                 updateInitialSendWindowSize(newInitialWindowSize);
378             }
379 
380         } else if (msg instanceof SpdyPingFrame) {
381 
382             /*
383              * SPDY PING frame processing requirements:
384              *
385              * Receivers of a PING frame should send an identical frame to the sender
386              * as soon as possible.
387              *
388              * Receivers of a PING frame must ignore frames that it did not initiate
389              */
390 
391             SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg;
392 
393             if (isRemoteInitiatedId(spdyPingFrame.id())) {
394                 ctx.writeAndFlush(spdyPingFrame);
395                 return;
396             }
397 
398             // Note: only checks that there are outstanding pings since uniqueness is not enforced
399             if (pings.get() == 0) {
400                 return;
401             }
402             pings.getAndDecrement();
403 
404         } else if (msg instanceof SpdyGoAwayFrame) {
405 
406             receivedGoAwayFrame = true;
407 
408         } else if (msg instanceof SpdyHeadersFrame) {
409 
410             SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
411             int streamId = spdyHeadersFrame.streamId();
412 
413             // Check if we received a valid HEADERS frame
414             if (spdyHeadersFrame.isInvalid()) {
415                 issueStreamError(ctx, streamId, SpdyStreamStatus.PROTOCOL_ERROR);
416                 return;
417             }
418 
419             if (spdySession.isRemoteSideClosed(streamId)) {
420                 issueStreamError(ctx, streamId, SpdyStreamStatus.INVALID_STREAM);
421                 return;
422             }
423 
424             // Close the remote side of the stream if this is the last frame
425             if (spdyHeadersFrame.isLast()) {
426                 halfCloseStream(streamId, true, ctx.newSucceededFuture());
427             }
428 
429         } else if (msg instanceof SpdyWindowUpdateFrame) {
430 
431             /*
432              * SPDY WINDOW_UPDATE frame processing requirements:
433              *
434              * Receivers of a WINDOW_UPDATE that cause the window size to exceed 2^31
435              * must send a RST_STREAM with the getStatus code FLOW_CONTROL_ERROR.
436              *
437              * Sender should ignore all WINDOW_UPDATE frames associated with a stream
438              * after sending the last frame for the stream.
439              */
440 
441             SpdyWindowUpdateFrame spdyWindowUpdateFrame = (SpdyWindowUpdateFrame) msg;
442             int streamId = spdyWindowUpdateFrame.streamId();
443             int deltaWindowSize = spdyWindowUpdateFrame.deltaWindowSize();
444 
445             // Ignore frames for half-closed streams
446             if (streamId != SPDY_SESSION_STREAM_ID && spdySession.isLocalSideClosed(streamId)) {
447                 return;
448             }
449 
450             // Check for numerical overflow
451             if (spdySession.getSendWindowSize(streamId) > Integer.MAX_VALUE - deltaWindowSize) {
452                 if (streamId == SPDY_SESSION_STREAM_ID) {
453                     issueSessionError(ctx, SpdySessionStatus.PROTOCOL_ERROR);
454                 } else {
455                     issueStreamError(ctx, streamId, SpdyStreamStatus.FLOW_CONTROL_ERROR);
456                 }
457                 return;
458             }
459 
460             updateSendWindowSize(ctx, streamId, deltaWindowSize);
461         }
462 
463         ctx.fireChannelRead(msg);
464     }
465 
466     @Override
467     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
468         for (Integer streamId: spdySession.activeStreams().keySet()) {
469             removeStream(streamId, ctx.newSucceededFuture());
470         }
471         ctx.fireChannelInactive();
472     }
473 
474     @Override
475     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
476         if (cause instanceof SpdyProtocolException) {
477             issueSessionError(ctx, SpdySessionStatus.PROTOCOL_ERROR);
478         }
479 
480         ctx.fireExceptionCaught(cause);
481     }
482 
483     @Override
484     public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
485         sendGoAwayFrame(ctx, promise);
486     }
487 
488     @Override
489     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
490         if (msg instanceof SpdyDataFrame ||
491             msg instanceof SpdySynStreamFrame ||
492             msg instanceof SpdySynReplyFrame ||
493             msg instanceof SpdyRstStreamFrame ||
494             msg instanceof SpdySettingsFrame ||
495             msg instanceof SpdyPingFrame ||
496             msg instanceof SpdyGoAwayFrame ||
497             msg instanceof SpdyHeadersFrame ||
498             msg instanceof SpdyWindowUpdateFrame) {
499 
500             handleOutboundMessage(ctx, msg, promise);
501         } else {
502             ctx.write(msg, promise);
503         }
504     }
505 
506     private void handleOutboundMessage(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
507         if (msg instanceof SpdyDataFrame) {
508 
509             SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
510             int streamId = spdyDataFrame.streamId();
511 
512             // Frames must not be sent on half-closed streams
513             if (spdySession.isLocalSideClosed(streamId)) {
514                 spdyDataFrame.release();
515                 promise.setFailure(PROTOCOL_EXCEPTION);
516                 return;
517             }
518 
519             /*
520              * SPDY Data frame flow control processing requirements:
521              *
522              * Sender must not send a data frame with data length greater
523              * than the transfer window size.
524              *
525              * After sending each data frame, the sender decrements its
526              * transfer window size by the amount of data transmitted.
527              *
528              * When the window size becomes less than or equal to 0, the
529              * sender must pause transmitting data frames.
530              */
531 
532             int dataLength = spdyDataFrame.content().readableBytes();
533             int sendWindowSize = spdySession.getSendWindowSize(streamId);
534             int sessionSendWindowSize = spdySession.getSendWindowSize(SPDY_SESSION_STREAM_ID);
535             sendWindowSize = Math.min(sendWindowSize, sessionSendWindowSize);
536 
537             if (sendWindowSize <= 0) {
538                 // Stream is stalled -- enqueue Data frame and return
539                 spdySession.putPendingWrite(streamId, new SpdySession.PendingWrite(spdyDataFrame, promise));
540                 return;
541             } else if (sendWindowSize < dataLength) {
542                 // Stream is not stalled but we cannot send the entire frame
543                 spdySession.updateSendWindowSize(streamId, -1 * sendWindowSize);
544                 spdySession.updateSendWindowSize(SPDY_SESSION_STREAM_ID, -1 * sendWindowSize);
545 
546                 // Create a partial data frame whose length is the current window size
547                 SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(
548                         streamId, spdyDataFrame.content().readRetainedSlice(sendWindowSize));
549 
550                 // Enqueue the remaining data (will be the first frame queued)
551                 spdySession.putPendingWrite(streamId, new SpdySession.PendingWrite(spdyDataFrame, promise));
552 
553                 // The transfer window size is pre-decremented when sending a data frame downstream.
554                 // Close the session on write failures that leave the transfer window in a corrupt state.
555                 final ChannelHandlerContext context = ctx;
556                 ctx.write(partialDataFrame).addListener(new ChannelFutureListener() {
557                     @Override
558                     public void operationComplete(ChannelFuture future) throws Exception {
559                         if (!future.isSuccess()) {
560                             issueSessionError(context, SpdySessionStatus.INTERNAL_ERROR);
561                         }
562                     }
563                 });
564                 return;
565             } else {
566                 // Window size is large enough to send entire data frame
567                 spdySession.updateSendWindowSize(streamId, -1 * dataLength);
568                 spdySession.updateSendWindowSize(SPDY_SESSION_STREAM_ID, -1 * dataLength);
569 
570                 // The transfer window size is pre-decremented when sending a data frame downstream.
571                 // Close the session on write failures that leave the transfer window in a corrupt state.
572                 final ChannelHandlerContext context = ctx;
573                 promise.addListener(new ChannelFutureListener() {
574                     @Override
575                     public void operationComplete(ChannelFuture future) throws Exception {
576                         if (!future.isSuccess()) {
577                             issueSessionError(context, SpdySessionStatus.INTERNAL_ERROR);
578                         }
579                     }
580                 });
581             }
582 
583             // Close the local side of the stream if this is the last frame
584             if (spdyDataFrame.isLast()) {
585                 halfCloseStream(streamId, false, promise);
586             }
587 
588         } else if (msg instanceof SpdySynStreamFrame) {
589 
590             SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg;
591             int streamId = spdySynStreamFrame.streamId();
592 
593             if (isRemoteInitiatedId(streamId)) {
594                 promise.setFailure(PROTOCOL_EXCEPTION);
595                 return;
596             }
597 
598             byte priority = spdySynStreamFrame.priority();
599             boolean remoteSideClosed = spdySynStreamFrame.isUnidirectional();
600             boolean localSideClosed = spdySynStreamFrame.isLast();
601             if (!acceptStream(streamId, priority, remoteSideClosed, localSideClosed)) {
602                 promise.setFailure(PROTOCOL_EXCEPTION);
603                 return;
604             }
605 
606         } else if (msg instanceof SpdySynReplyFrame) {
607 
608             SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg;
609             int streamId = spdySynReplyFrame.streamId();
610 
611             // Frames must not be sent on half-closed streams
612             if (!isRemoteInitiatedId(streamId) || spdySession.isLocalSideClosed(streamId)) {
613                 promise.setFailure(PROTOCOL_EXCEPTION);
614                 return;
615             }
616 
617             // Close the local side of the stream if this is the last frame
618             if (spdySynReplyFrame.isLast()) {
619                 halfCloseStream(streamId, false, promise);
620             }
621 
622         } else if (msg instanceof SpdyRstStreamFrame) {
623 
624             SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
625             removeStream(spdyRstStreamFrame.streamId(), promise);
626 
627         } else if (msg instanceof SpdySettingsFrame) {
628 
629             SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg;
630 
631             int settingsMinorVersion = spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MINOR_VERSION);
632             if (settingsMinorVersion >= 0 && settingsMinorVersion != minorVersion) {
633                 // Settings frame had the wrong minor version
634                 promise.setFailure(PROTOCOL_EXCEPTION);
635                 return;
636             }
637 
638             int newConcurrentStreams =
639                     spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS);
640             if (newConcurrentStreams >= 0) {
641                 localConcurrentStreams = newConcurrentStreams;
642             }
643 
644             // Persistence flag are inconsistent with the use of SETTINGS to communicate
645             // the initial window size. Remove flags from the sender requesting that the
646             // value be persisted. Remove values that the sender indicates are persisted.
647             if (spdySettingsFrame.isPersisted(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE)) {
648                 spdySettingsFrame.removeValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE);
649             }
650             spdySettingsFrame.setPersistValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE, false);
651 
652             int newInitialWindowSize =
653                     spdySettingsFrame.getValue(SpdySettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE);
654             if (newInitialWindowSize >= 0) {
655                 updateInitialReceiveWindowSize(newInitialWindowSize);
656             }
657 
658         } else if (msg instanceof SpdyPingFrame) {
659 
660             SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg;
661             if (isRemoteInitiatedId(spdyPingFrame.id())) {
662                 ctx.fireExceptionCaught(new IllegalArgumentException(
663                             "invalid PING ID: " + spdyPingFrame.id()));
664                 return;
665             }
666             pings.getAndIncrement();
667 
668         } else if (msg instanceof SpdyGoAwayFrame) {
669 
670             // Why is this being sent? Intercept it and fail the write.
671             // Should have sent a CLOSE ChannelStateEvent
672             promise.setFailure(PROTOCOL_EXCEPTION);
673             return;
674 
675         } else if (msg instanceof SpdyHeadersFrame) {
676 
677             SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
678             int streamId = spdyHeadersFrame.streamId();
679 
680             // Frames must not be sent on half-closed streams
681             if (spdySession.isLocalSideClosed(streamId)) {
682                 promise.setFailure(PROTOCOL_EXCEPTION);
683                 return;
684             }
685 
686             // Close the local side of the stream if this is the last frame
687             if (spdyHeadersFrame.isLast()) {
688                 halfCloseStream(streamId, false, promise);
689             }
690 
691         } else if (msg instanceof SpdyWindowUpdateFrame) {
692 
693             // Why is this being sent? Intercept it and fail the write.
694             promise.setFailure(PROTOCOL_EXCEPTION);
695             return;
696         }
697 
698         ctx.write(msg, promise);
699     }
700 
701     /*
702      * SPDY Session Error Handling:
703      *
704      * When a session error occurs, the endpoint encountering the error must first
705      * send a GOAWAY frame with the Stream-ID of the most recently received stream
706      * from the remote endpoint, and the error code for why the session is terminating.
707      *
708      * After sending the GOAWAY frame, the endpoint must close the TCP connection.
709      */
710     private void issueSessionError(
711             ChannelHandlerContext ctx, SpdySessionStatus status) {
712 
713         sendGoAwayFrame(ctx, status).addListener(new ClosingChannelFutureListener(ctx, ctx.newPromise()));
714     }
715 
716     /*
717      * SPDY Stream Error Handling:
718      *
719      * Upon a stream error, the endpoint must send a RST_STREAM frame which contains
720      * the Stream-ID for the stream where the error occurred and the error getStatus which
721      * caused the error.
722      *
723      * After sending the RST_STREAM, the stream is closed to the sending endpoint.
724      *
725      * Note: this is only called by the worker thread
726      */
727     private void issueStreamError(ChannelHandlerContext ctx, int streamId, SpdyStreamStatus status) {
728         boolean fireChannelRead = !spdySession.isRemoteSideClosed(streamId);
729         ChannelPromise promise = ctx.newPromise();
730         removeStream(streamId, promise);
731 
732         SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamId, status);
733         ctx.writeAndFlush(spdyRstStreamFrame, promise);
734         if (fireChannelRead) {
735             ctx.fireChannelRead(spdyRstStreamFrame);
736         }
737     }
738 
739     /*
740      * Helper functions
741      */
742 
743     private boolean isRemoteInitiatedId(int id) {
744         boolean serverId = isServerId(id);
745         return server && !serverId || !server && serverId;
746     }
747 
748     // need to synchronize to prevent new streams from being created while updating active streams
749     private void updateInitialSendWindowSize(int newInitialWindowSize) {
750         int deltaWindowSize = newInitialWindowSize - initialSendWindowSize;
751         initialSendWindowSize = newInitialWindowSize;
752         spdySession.updateAllSendWindowSizes(deltaWindowSize);
753     }
754 
755     // need to synchronize to prevent new streams from being created while updating active streams
756     private void updateInitialReceiveWindowSize(int newInitialWindowSize) {
757         int deltaWindowSize = newInitialWindowSize - initialReceiveWindowSize;
758         initialReceiveWindowSize = newInitialWindowSize;
759         spdySession.updateAllReceiveWindowSizes(deltaWindowSize);
760     }
761 
762     // need to synchronize accesses to sentGoAwayFrame, lastGoodStreamId, and initial window sizes
763     private boolean acceptStream(
764             int streamId, byte priority, boolean remoteSideClosed, boolean localSideClosed) {
765         // Cannot initiate any new streams after receiving or sending GOAWAY
766         if (receivedGoAwayFrame || sentGoAwayFrame) {
767             return false;
768         }
769 
770         boolean remote = isRemoteInitiatedId(streamId);
771         int maxConcurrentStreams = remote ? localConcurrentStreams : remoteConcurrentStreams;
772         if (spdySession.numActiveStreams(remote) >= maxConcurrentStreams) {
773             return false;
774         }
775         spdySession.acceptStream(
776                 streamId, priority, remoteSideClosed, localSideClosed,
777                 initialSendWindowSize, initialReceiveWindowSize, remote);
778         if (remote) {
779             lastGoodStreamId = streamId;
780         }
781         return true;
782     }
783 
784     private void halfCloseStream(int streamId, boolean remote, ChannelFuture future) {
785         if (remote) {
786             spdySession.closeRemoteSide(streamId, isRemoteInitiatedId(streamId));
787         } else {
788             spdySession.closeLocalSide(streamId, isRemoteInitiatedId(streamId));
789         }
790         if (closeSessionFutureListener != null && spdySession.noActiveStreams()) {
791             future.addListener(closeSessionFutureListener);
792         }
793     }
794 
795     private void removeStream(int streamId, ChannelFuture future) {
796         spdySession.removeStream(streamId, STREAM_CLOSED, isRemoteInitiatedId(streamId));
797 
798         if (closeSessionFutureListener != null && spdySession.noActiveStreams()) {
799             future.addListener(closeSessionFutureListener);
800         }
801     }
802 
803     private void updateSendWindowSize(final ChannelHandlerContext ctx, int streamId, int deltaWindowSize) {
804         spdySession.updateSendWindowSize(streamId, deltaWindowSize);
805 
806         while (true) {
807             // Check if we have unblocked a stalled stream
808             SpdySession.PendingWrite pendingWrite = spdySession.getPendingWrite(streamId);
809             if (pendingWrite == null) {
810                 return;
811             }
812 
813             SpdyDataFrame spdyDataFrame = pendingWrite.spdyDataFrame;
814             int dataFrameSize = spdyDataFrame.content().readableBytes();
815             int writeStreamId = spdyDataFrame.streamId();
816             int sendWindowSize = spdySession.getSendWindowSize(writeStreamId);
817             int sessionSendWindowSize = spdySession.getSendWindowSize(SPDY_SESSION_STREAM_ID);
818             sendWindowSize = Math.min(sendWindowSize, sessionSendWindowSize);
819 
820             if (sendWindowSize <= 0) {
821                 return;
822             } else if (sendWindowSize < dataFrameSize) {
823                 // We can send a partial frame
824                 spdySession.updateSendWindowSize(writeStreamId, -1 * sendWindowSize);
825                 spdySession.updateSendWindowSize(SPDY_SESSION_STREAM_ID, -1 * sendWindowSize);
826 
827                 // Create a partial data frame whose length is the current window size
828                 SpdyDataFrame partialDataFrame = new DefaultSpdyDataFrame(
829                         writeStreamId, spdyDataFrame.content().readRetainedSlice(sendWindowSize));
830 
831                 // The transfer window size is pre-decremented when sending a data frame downstream.
832                 // Close the session on write failures that leave the transfer window in a corrupt state.
833                 ctx.writeAndFlush(partialDataFrame).addListener(new ChannelFutureListener() {
834                     @Override
835                     public void operationComplete(ChannelFuture future) throws Exception {
836                         if (!future.isSuccess()) {
837                             issueSessionError(ctx, SpdySessionStatus.INTERNAL_ERROR);
838                         }
839                     }
840                 });
841             } else {
842                 // Window size is large enough to send entire data frame
843                 spdySession.removePendingWrite(writeStreamId);
844                 spdySession.updateSendWindowSize(writeStreamId, -1 * dataFrameSize);
845                 spdySession.updateSendWindowSize(SPDY_SESSION_STREAM_ID, -1 * dataFrameSize);
846 
847                 // Close the local side of the stream if this is the last frame
848                 if (spdyDataFrame.isLast()) {
849                     halfCloseStream(writeStreamId, false, pendingWrite.promise);
850                 }
851 
852                 // The transfer window size is pre-decremented when sending a data frame downstream.
853                 // Close the session on write failures that leave the transfer window in a corrupt state.
854                 ctx.writeAndFlush(spdyDataFrame, pendingWrite.promise).addListener(new ChannelFutureListener() {
855                     @Override
856                     public void operationComplete(ChannelFuture future) throws Exception {
857                         if (!future.isSuccess()) {
858                             issueSessionError(ctx, SpdySessionStatus.INTERNAL_ERROR);
859                         }
860                     }
861                 });
862             }
863         }
864     }
865 
866     private void sendGoAwayFrame(ChannelHandlerContext ctx, ChannelPromise future) {
867         // Avoid NotYetConnectedException
868         if (!ctx.channel().isActive()) {
869             ctx.close(future);
870             return;
871         }
872 
873         ChannelFuture f = sendGoAwayFrame(ctx, SpdySessionStatus.OK);
874         if (spdySession.noActiveStreams()) {
875             f.addListener(new ClosingChannelFutureListener(ctx, future));
876         } else {
877             closeSessionFutureListener = new ClosingChannelFutureListener(ctx, future);
878         }
879         // FIXME: Close the connection forcibly after timeout.
880     }
881 
882     private ChannelFuture sendGoAwayFrame(
883             ChannelHandlerContext ctx, SpdySessionStatus status) {
884         if (!sentGoAwayFrame) {
885             sentGoAwayFrame = true;
886             SpdyGoAwayFrame spdyGoAwayFrame = new DefaultSpdyGoAwayFrame(lastGoodStreamId, status);
887             return ctx.writeAndFlush(spdyGoAwayFrame);
888         } else {
889             return ctx.newSucceededFuture();
890         }
891     }
892 
893     private static final class ClosingChannelFutureListener implements ChannelFutureListener {
894         private final ChannelHandlerContext ctx;
895         private final ChannelPromise promise;
896 
897         ClosingChannelFutureListener(ChannelHandlerContext ctx, ChannelPromise promise) {
898             this.ctx = ctx;
899             this.promise = promise;
900         }
901 
902         @Override
903         public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception {
904             ctx.close(promise);
905         }
906     }
907 }