/*
* 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.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.handler.codec.embedder.DecoderEmbedder;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.jboss.netty.handler.codec.spdy.SpdyCodecUtil.*;
import static org.junit.Assert.*;
public class SpdySessionHandlerTest {
private static final int closeSignal = SPDY_SETTINGS_MAX_ID;
private static final SpdySettingsFrame closeMessage = new DefaultSpdySettingsFrame();
static {
closeMessage.setValue(closeSignal, 0);
}
private static void assertDataFrame(Object msg, int streamId, boolean last) {
assertNotNull(msg);
assertTrue(msg instanceof SpdyDataFrame);
SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg;
assertEquals(spdyDataFrame.getStreamId(), streamId);
assertEquals(spdyDataFrame.isLast(), last);
}
private static void assertSynReply(Object msg, int streamId, boolean last, SpdyHeadersFrame headers) {
assertNotNull(msg);
assertTrue(msg instanceof SpdySynReplyFrame);
assertHeaders(msg, streamId, last, headers);
}
private static void assertRstStream(Object msg, int streamId, SpdyStreamStatus status) {
assertNotNull(msg);
assertTrue(msg instanceof SpdyRstStreamFrame);
SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg;
assertEquals(spdyRstStreamFrame.getStreamId(), streamId);
assertEquals(spdyRstStreamFrame.getStatus(), status);
}
private static void assertPing(Object msg, int id) {
assertNotNull(msg);
assertTrue(msg instanceof SpdyPingFrame);
SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg;
assertEquals(spdyPingFrame.getId(), id);
}
private static void assertGoAway(Object msg, int lastGoodStreamId) {
assertNotNull(msg);
assertTrue(msg instanceof SpdyGoAwayFrame);
SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg;
assertEquals(spdyGoAwayFrame.getLastGoodStreamId(), lastGoodStreamId);
}
private static void assertHeaders(Object msg, int streamId, boolean last, SpdyHeadersFrame headers) {
assertNotNull(msg);
assertTrue(msg instanceof SpdyHeadersFrame);
SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg;
assertEquals(spdyHeadersFrame.getStreamId(), streamId);
assertEquals(spdyHeadersFrame.isLast(), last);
for (String name: headers.getHeaderNames()) {
List<String> expectedValues = headers.getHeaders(name);
List<String> receivedValues = spdyHeadersFrame.getHeaders(name);
assertTrue(receivedValues.containsAll(expectedValues));
receivedValues.removeAll(expectedValues);
assertTrue(receivedValues.isEmpty());
spdyHeadersFrame.removeHeader(name);
}
assertTrue(spdyHeadersFrame.getHeaders().isEmpty());
}
private static void testSpdySessionHandler(int version, boolean server) {
DecoderEmbedder<Object> sessionHandler =
new DecoderEmbedder<Object>(
new SpdySessionHandler(version, server), new EchoHandler(closeSignal, server));
sessionHandler.pollAll();
int localStreamId = server ? 1 : 2;
int remoteStreamId = server ? 2 : 1;
SpdyPingFrame localPingFrame = new DefaultSpdyPingFrame(localStreamId);
SpdyPingFrame remotePingFrame = new DefaultSpdyPingFrame(remoteStreamId);
SpdySynStreamFrame spdySynStreamFrame = new DefaultSpdySynStreamFrame(localStreamId, 0, (byte) 0);
spdySynStreamFrame.setHeader("Compression", "test");
SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(localStreamId);
spdyDataFrame.setLast(true);
// Check if session handler returns INVALID_STREAM if it receives
// a data frame for a Stream-ID that is not open
sessionHandler.offer(new DefaultSpdyDataFrame(localStreamId));
assertRstStream(sessionHandler.poll(), localStreamId, SpdyStreamStatus.INVALID_STREAM);
assertNull(sessionHandler.peek());
// Check if session handler returns PROTOCOL_ERROR if it receives
// a data frame for a Stream-ID before receiving a SYN_REPLY frame
sessionHandler.offer(new DefaultSpdyDataFrame(remoteStreamId));
assertRstStream(sessionHandler.poll(), remoteStreamId, SpdyStreamStatus.PROTOCOL_ERROR);
assertNull(sessionHandler.peek());
remoteStreamId += 2;
// Check if session handler correctly limits the number of
// concurrent streams in the SETTINGS frame
SpdySettingsFrame spdySettingsFrame = new DefaultSpdySettingsFrame();
spdySettingsFrame.setValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS, 0);
sessionHandler.offer(spdySettingsFrame);
assertNull(sessionHandler.peek());
sessionHandler.offer(spdySynStreamFrame);
assertRstStream(sessionHandler.poll(), localStreamId, SpdyStreamStatus.REFUSED_STREAM);
assertNull(sessionHandler.peek());
spdySettingsFrame.setValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS, 100);
sessionHandler.offer(spdySettingsFrame);
assertNull(sessionHandler.peek());
sessionHandler.offer(new DefaultSpdySynReplyFrame(remoteStreamId));
assertNull(sessionHandler.peek());
// Check if session handler returns PROTOCOL_ERROR if it receives
// multiple SYN_REPLY frames for the same active Stream-ID
sessionHandler.offer(new DefaultSpdySynReplyFrame(remoteStreamId));
assertRstStream(sessionHandler.poll(), remoteStreamId, SpdyStreamStatus.STREAM_IN_USE);
assertNull(sessionHandler.peek());
remoteStreamId += 2;
// Check if frame codec correctly compresses/uncompresses headers
sessionHandler.offer(spdySynStreamFrame);
assertSynReply(sessionHandler.poll(), localStreamId, false, spdySynStreamFrame);
assertNull(sessionHandler.peek());
SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(localStreamId);
spdyHeadersFrame.addHeader("HEADER","test1");
spdyHeadersFrame.addHeader("HEADER","test2");
sessionHandler.offer(spdyHeadersFrame);
assertHeaders(sessionHandler.poll(), localStreamId, false, spdyHeadersFrame);
assertNull(sessionHandler.peek());
localStreamId += 2;
// Check if session handler closed the streams using the number
// of concurrent streams and that it returns REFUSED_STREAM
// if it receives a SYN_STREAM frame it does not wish to accept
spdySynStreamFrame.setStreamId(localStreamId);
spdySynStreamFrame.setLast(true);
spdySynStreamFrame.setUnidirectional(true);
sessionHandler.offer(spdySynStreamFrame);
assertRstStream(sessionHandler.poll(), localStreamId, SpdyStreamStatus.REFUSED_STREAM);
assertNull(sessionHandler.peek());
// Check if session handler drops active streams if it receives
// a RST_STREAM frame for that Stream-ID
sessionHandler.offer(new DefaultSpdyRstStreamFrame(remoteStreamId, 3));
assertNull(sessionHandler.peek());
remoteStreamId += 2;
// Check if session handler honors UNIDIRECTIONAL streams
spdySynStreamFrame.setLast(false);
sessionHandler.offer(spdySynStreamFrame);
assertNull(sessionHandler.peek());
spdySynStreamFrame.setUnidirectional(false);
// Check if session handler returns PROTOCOL_ERROR if it receives
// multiple SYN_STREAM frames for the same active Stream-ID
sessionHandler.offer(spdySynStreamFrame);
assertRstStream(sessionHandler.poll(), localStreamId, SpdyStreamStatus.PROTOCOL_ERROR);
assertNull(sessionHandler.peek());
localStreamId += 2;
// Check if session handler returns PROTOCOL_ERROR if it receives
// a SYN_STREAM frame with an invalid Stream-ID
spdySynStreamFrame.setStreamId(localStreamId - 1);
sessionHandler.offer(spdySynStreamFrame);
assertRstStream(sessionHandler.poll(), localStreamId - 1, SpdyStreamStatus.PROTOCOL_ERROR);
assertNull(sessionHandler.peek());
spdySynStreamFrame.setStreamId(localStreamId);
// Check if session handler correctly handles updates to the max
// concurrent streams in the SETTINGS frame
spdySettingsFrame.setValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS, 2);
sessionHandler.offer(spdySettingsFrame);
assertNull(sessionHandler.peek());
sessionHandler.offer(spdySynStreamFrame);
assertRstStream(sessionHandler.poll(), localStreamId, SpdyStreamStatus.REFUSED_STREAM);
assertNull(sessionHandler.peek());
spdySettingsFrame.setValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS, 4);
sessionHandler.offer(spdySettingsFrame);
assertNull(sessionHandler.peek());
sessionHandler.offer(spdySynStreamFrame);
assertSynReply(sessionHandler.poll(), localStreamId, false, spdySynStreamFrame);
assertNull(sessionHandler.peek());
// Check if session handler rejects HEADERS for closed streams
int testStreamId = spdyDataFrame.getStreamId();
sessionHandler.offer(spdyDataFrame);
assertDataFrame(sessionHandler.poll(), testStreamId, spdyDataFrame.isLast());
assertNull(sessionHandler.peek());
spdyHeadersFrame.setStreamId(testStreamId);
sessionHandler.offer(spdyHeadersFrame);
assertRstStream(sessionHandler.poll(), testStreamId, SpdyStreamStatus.INVALID_STREAM);
assertNull(sessionHandler.peek());
// Check if session handler returns PROTOCOL_ERROR if it receives
// an invalid HEADERS frame
spdyHeadersFrame.setStreamId(localStreamId);
spdyHeadersFrame.setInvalid();
sessionHandler.offer(spdyHeadersFrame);
assertRstStream(sessionHandler.poll(), localStreamId, SpdyStreamStatus.PROTOCOL_ERROR);
assertNull(sessionHandler.peek());
// Check if session handler returns identical local PINGs
sessionHandler.offer(localPingFrame);
assertPing(sessionHandler.poll(), localPingFrame.getId());
assertNull(sessionHandler.peek());
// Check if session handler ignores un-initiated remote PINGs
sessionHandler.offer(remotePingFrame);
assertNull(sessionHandler.peek());
// Check if session handler sends a GOAWAY frame when closing
sessionHandler.offer(closeMessage);
assertGoAway(sessionHandler.poll(), localStreamId);
assertNull(sessionHandler.peek());
localStreamId += 2;
// Check if session handler returns REFUSED_STREAM if it receives
// SYN_STREAM frames after sending a GOAWAY frame
spdySynStreamFrame.setStreamId(localStreamId);
sessionHandler.offer(spdySynStreamFrame);
assertRstStream(sessionHandler.poll(), localStreamId, SpdyStreamStatus.REFUSED_STREAM);
assertNull(sessionHandler.peek());
// Check if session handler ignores Data frames after sending
// a GOAWAY frame
spdyDataFrame.setStreamId(localStreamId);
sessionHandler.offer(spdyDataFrame);
assertNull(sessionHandler.peek());
sessionHandler.finish();
}
@Test
public void testSpdyClientSessionHandler() {
for (int version = SPDY_MIN_VERSION; version <= SPDY_MAX_VERSION; version ++) {
testSpdySessionHandler(version, false);
}
}
@Test
public void testSpdyServerSessionHandler() {
for (int version = SPDY_MIN_VERSION; version <= SPDY_MAX_VERSION; version ++) {
testSpdySessionHandler(version, true);
}
}
// Echo Handler opens 4 half-closed streams on session connection
// and then sets the number of concurrent streams to 3
private static class EchoHandler extends SimpleChannelUpstreamHandler {
private final int closeSignal;
private final boolean server;
EchoHandler(int closeSignal, boolean server) {
this.closeSignal = closeSignal;
this.server = server;
}
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
// Initiate 4 new streams
int streamId = server ? 2 : 1;
SpdySynStreamFrame spdySynStreamFrame = new DefaultSpdySynStreamFrame(streamId, 0, (byte) 0);
spdySynStreamFrame.setLast(true);
Channels.write(e.getChannel(), spdySynStreamFrame);
spdySynStreamFrame.setStreamId(spdySynStreamFrame.getStreamId() + 2);
Channels.write(e.getChannel(), spdySynStreamFrame);
spdySynStreamFrame.setStreamId(spdySynStreamFrame.getStreamId() + 2);
Channels.write(e.getChannel(), spdySynStreamFrame);
spdySynStreamFrame.setStreamId(spdySynStreamFrame.getStreamId() + 2);
Channels.write(e.getChannel(), spdySynStreamFrame);
// Limit the number of concurrent streams to 3
SpdySettingsFrame spdySettingsFrame = new DefaultSpdySettingsFrame();
spdySettingsFrame.setValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS, 3);
Channels.write(e.getChannel(), spdySettingsFrame);
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
throws Exception {
Object msg = e.getMessage();
if (msg instanceof SpdySynStreamFrame) {
SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg;
int streamId = spdySynStreamFrame.getStreamId();
SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
spdySynReplyFrame.setLast(spdySynStreamFrame.isLast());
for (Map.Entry<String, String> entry: spdySynStreamFrame.getHeaders()) {
spdySynReplyFrame.addHeader(entry.getKey(), entry.getValue());
}
Channels.write(e.getChannel(), spdySynReplyFrame, e.getRemoteAddress());
return;
}
if (msg instanceof SpdySynReplyFrame) {
return;
}
if (msg instanceof SpdyDataFrame ||
msg instanceof SpdyPingFrame ||
msg instanceof SpdyHeadersFrame) {
Channels.write(e.getChannel(), msg, e.getRemoteAddress());
return;
}
if (msg instanceof SpdySettingsFrame) {
SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg;
if (spdySettingsFrame.isSet(closeSignal)) {
Channels.close(e.getChannel());
}
}
}
}
}