From 58e6bec3b1a85fb745012e0310cb366e72203c08 Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Sat, 4 Feb 2012 17:31:53 +0100 Subject: [PATCH] Merge SPDY support from 3.2 branch. See #176 --- .../codec/spdy/DefaultSpdyDataFrame.java | 101 ++++ .../codec/spdy/DefaultSpdyGoAwayFrame.java | 57 ++ .../codec/spdy/DefaultSpdyHeaderBlock.java | 96 ++++ .../codec/spdy/DefaultSpdyHeadersFrame.java | 66 +++ .../codec/spdy/DefaultSpdyNoOpFrame.java | 33 ++ .../codec/spdy/DefaultSpdyPingFrame.java | 53 ++ .../codec/spdy/DefaultSpdyRstStreamFrame.java | 81 +++ .../codec/spdy/DefaultSpdySettingsFrame.java | 188 +++++++ .../codec/spdy/DefaultSpdySynReplyFrame.java | 78 +++ .../codec/spdy/DefaultSpdySynStreamFrame.java | 129 +++++ .../handler/codec/spdy/SpdyCodecUtil.java | 176 ++++++ .../handler/codec/spdy/SpdyDataFrame.java | 69 +++ .../handler/codec/spdy/SpdyFrameCodec.java | 46 ++ .../handler/codec/spdy/SpdyFrameDecoder.java | 322 +++++++++++ .../handler/codec/spdy/SpdyFrameEncoder.java | 257 +++++++++ .../handler/codec/spdy/SpdyGoAwayFrame.java | 33 ++ .../handler/codec/spdy/SpdyHeaderBlock.java | 103 ++++ .../netty/handler/codec/spdy/SpdyHeaders.java | 530 ++++++++++++++++++ .../handler/codec/spdy/SpdyHeadersFrame.java | 32 ++ .../handler/codec/spdy/SpdyNoOpFrame.java | 22 + .../handler/codec/spdy/SpdyPingFrame.java | 32 ++ .../codec/spdy/SpdyProtocolException.java | 52 ++ .../codec/spdy/SpdyRstStreamFrame.java | 42 ++ .../netty/handler/codec/spdy/SpdySession.java | 133 +++++ .../codec/spdy/SpdySessionHandler.java | 497 ++++++++++++++++ .../handler/codec/spdy/SpdySettingsFrame.java | 105 ++++ .../handler/codec/spdy/SpdyStreamStatus.java | 155 +++++ .../handler/codec/spdy/SpdySynReplyFrame.java | 43 ++ .../codec/spdy/SpdySynStreamFrame.java | 77 +++ .../handler/codec/spdy/package-info.java | 28 + .../spdy/AbstractSocketSpdyEchoTest.java | 252 +++++++++ .../codec/spdy/NioNioSocketSpdyEchoTest.java | 36 ++ .../codec/spdy/NioOioSocketSpdyEchoTest.java | 36 ++ .../codec/spdy/OioNioSocketSpdyEchoTest.java | 36 ++ .../codec/spdy/OioOioSocketSpdyEchoTest.java | 36 ++ .../codec/spdy/SpdySessionHandlerTest.java | 334 +++++++++++ 36 files changed, 4366 insertions(+) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyPingFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyRstStreamFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySettingsFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlock.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyPingFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyRstStreamFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySession.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynReplyFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/spdy/package-info.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/spdy/NioNioSocketSpdyEchoTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/spdy/NioOioSocketSpdyEchoTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/spdy/OioNioSocketSpdyEchoTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/spdy/OioOioSocketSpdyEchoTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java new file mode 100644 index 0000000000..2a2ca7cba0 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyDataFrame} implementation. + */ +public class DefaultSpdyDataFrame implements SpdyDataFrame { + + private int streamID; + private boolean last; + private boolean compressed; + private ChannelBuffer data = ChannelBuffers.EMPTY_BUFFER; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + */ + public DefaultSpdyDataFrame(int streamID) { + setStreamID(streamID); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public boolean isLast() { + return last; + } + + public void setLast(boolean last) { + this.last = last; + } + + public boolean isCompressed() { + return compressed; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + + public ChannelBuffer getData() { + return data; + } + + public void setData(ChannelBuffer data) { + if (data == null) { + data = ChannelBuffers.EMPTY_BUFFER; + } + if (data.readableBytes() > SpdyCodecUtil.SPDY_MAX_LENGTH) { + throw new IllegalArgumentException("data payload cannot exceed " + + SpdyCodecUtil.SPDY_MAX_LENGTH + " bytes"); + } + this.data = data; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append("(last: "); + buf.append(isLast()); + buf.append("; compressed: "); + buf.append(isCompressed()); + buf.append(')'); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Size = "); + buf.append(data.readableBytes()); + return buf.toString(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java new file mode 100644 index 0000000000..d7552f82a3 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyGoAwayFrame} implementation. + */ +public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame { + + private int lastGoodStreamID; + + /** + * Creates a new instance. + * + * @param lastGoodStreamID the Last-good-stream-ID of this frame + */ + public DefaultSpdyGoAwayFrame(int lastGoodStreamID) { + setLastGoodStreamID(lastGoodStreamID); + } + + public int getLastGoodStreamID() { + return lastGoodStreamID; + } + + public void setLastGoodStreamID(int lastGoodStreamID) { + if (lastGoodStreamID < 0) { + throw new IllegalArgumentException("Last-good-stream-ID" + + " cannot be negative: " + lastGoodStreamID); + } + this.lastGoodStreamID = lastGoodStreamID; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> Last-good-stream-ID = "); + buf.append(lastGoodStreamID); + return buf.toString(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java new file mode 100644 index 0000000000..b181a65747 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyHeaderBlock} implementation. + */ +public class DefaultSpdyHeaderBlock implements SpdyHeaderBlock { + + private boolean invalid; + private final SpdyHeaders headers = new SpdyHeaders(); + + /** + * Creates a new instance. + */ + protected DefaultSpdyHeaderBlock() { + } + + public boolean isInvalid() { + return invalid; + } + + public void setInvalid() { + this.invalid = true; + } + + public void addHeader(final String name, final Object value) { + headers.addHeader(name, value); + } + + public void setHeader(final String name, final Object value) { + headers.setHeader(name, value); + } + + public void setHeader(final String name, final Iterable values) { + headers.setHeader(name, values); + } + + public void removeHeader(final String name) { + headers.removeHeader(name); + } + + public void clearHeaders() { + headers.clearHeaders(); + } + + public String getHeader(final String name) { + List values = getHeaders(name); + return values.size() > 0 ? values.get(0) : null; + } + + public List getHeaders(final String name) { + return headers.getHeaders(name); + } + + public List> getHeaders() { + return headers.getHeaders(); + } + + public boolean containsHeader(final String name) { + return headers.containsHeader(name); + } + + public Set getHeaderNames() { + return headers.getHeaderNames(); + } + + protected void appendHeaders(StringBuilder buf) { + for (Map.Entry e: getHeaders()) { + buf.append(" "); + buf.append(e.getKey()); + buf.append(": "); + buf.append(e.getValue()); + buf.append(StringUtil.NEWLINE); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java new file mode 100644 index 0000000000..e969e163ec --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyHeadersFrame} implementation. + */ +public class DefaultSpdyHeadersFrame extends DefaultSpdyHeaderBlock + implements SpdyHeadersFrame { + + private int streamID; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + */ + public DefaultSpdyHeadersFrame(int streamID) { + super(); + setStreamID(streamID); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Headers:"); + buf.append(StringUtil.NEWLINE); + appendHeaders(buf); + + // Remove the last newline. + buf.setLength(buf.length() - StringUtil.NEWLINE.length()); + return buf.toString(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java new file mode 100644 index 0000000000..c3aaab4ebc --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * The default {@link SpdyNoOpFrame} implementation. + */ +public class DefaultSpdyNoOpFrame implements SpdyNoOpFrame { + + /** + * Creates a new instance. + */ + public DefaultSpdyNoOpFrame() { + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyPingFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyPingFrame.java new file mode 100644 index 0000000000..1000ef4ce7 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyPingFrame.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyPingFrame} implementation. + */ +public class DefaultSpdyPingFrame implements SpdyPingFrame { + + private int ID; + + /** + * Creates a new instance. + * + * @param ID the unique ID of this frame + */ + public DefaultSpdyPingFrame(int ID) { + setID(ID); + } + + public int getID() { + return ID; + } + + public void setID(int ID) { + this.ID = ID; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> ID = "); + buf.append(ID); + return buf.toString(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyRstStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyRstStreamFrame.java new file mode 100644 index 0000000000..7164ac9719 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyRstStreamFrame.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyRstStreamFrame} implementation. + */ +public class DefaultSpdyRstStreamFrame implements SpdyRstStreamFrame { + + private int streamID; + private SpdyStreamStatus status; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + * @param statusCode the Status code of this frame + */ + public DefaultSpdyRstStreamFrame(int streamID, int statusCode) { + this(streamID, SpdyStreamStatus.valueOf(statusCode)); + } + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + * @param status the status of this frame + */ + public DefaultSpdyRstStreamFrame(int streamID, SpdyStreamStatus status) { + setStreamID(streamID); + setStatus(status); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public SpdyStreamStatus getStatus() { + return status; + } + + public void setStatus(SpdyStreamStatus status) { + this.status = status; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Status: "); + buf.append(status.toString()); + return buf.toString(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySettingsFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySettingsFrame.java new file mode 100644 index 0000000000..9d120f7a8f --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySettingsFrame.java @@ -0,0 +1,188 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdySettingsFrame} implementation. + */ +public class DefaultSpdySettingsFrame implements SpdySettingsFrame { + + private boolean clear; + private final Map settingsMap = new TreeMap(); + + /** + * Creates a new instance. + */ + public DefaultSpdySettingsFrame() { + } + + public Set getIDs() { + return settingsMap.keySet(); + } + + public boolean isSet(int ID) { + Integer key = new Integer(ID); + return settingsMap.containsKey(key); + } + + public int getValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getValue(); + } else { + return -1; + } + } + + public void setValue(int ID, int value) { + setValue(ID, value, false, false); + } + + public void setValue(int ID, int value, boolean persistValue, boolean persisted) { + if (ID <= 0 || ID > SpdyCodecUtil.SPDY_SETTINGS_MAX_ID) { + throw new IllegalArgumentException("Setting ID is not valid: " + ID); + } + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + Setting setting = settingsMap.get(key); + setting.setValue(value); + setting.setPersist(persistValue); + setting.setPersisted(persisted); + } else { + settingsMap.put(key, new Setting(value, persistValue, persisted)); + } + } + + public void removeValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.remove(key); + } + } + + public boolean persistValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getPersist(); + } else { + return false; + } + } + + public void setPersistValue(int ID, boolean persistValue) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.get(key).setPersist(persistValue); + } + } + + public boolean isPersisted(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getPersisted(); + } else { + return false; + } + } + + public void setPersisted(int ID, boolean persisted) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.get(key).setPersisted(persisted); + } + } + + public boolean clearPreviouslyPersistedSettings() { + return clear; + } + + public void setClearPreviouslyPersistedSettings(boolean clear) { + this.clear = clear; + } + + private Set> getSettings() { + return settingsMap.entrySet(); + } + + private void appendSettings(StringBuilder buf) { + for (Map.Entry e: getSettings()) { + Setting setting = e.getValue(); + buf.append("--> "); + buf.append(e.getKey().toString()); + buf.append(":"); + buf.append(setting.getValue()); + buf.append(" (persist value: "); + buf.append(setting.getPersist()); + buf.append("; persisted: "); + buf.append(setting.getPersisted()); + buf.append(')'); + buf.append(StringUtil.NEWLINE); + } + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + appendSettings(buf); + buf.setLength(buf.length() - StringUtil.NEWLINE.length()); + return buf.toString(); + } + + private static final class Setting { + + private int value; + private boolean persist; + private boolean persisted; + + public Setting(int value, boolean persist, boolean persisted) { + this.value = value; + this.persist = persist; + this.persisted = persisted; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public boolean getPersist() { + return persist; + } + + public void setPersist(boolean persist) { + this.persist = persist; + } + + public boolean getPersisted() { + return persisted; + } + + public void setPersisted(boolean persisted) { + this.persisted = persisted; + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java new file mode 100644 index 0000000000..2f99afffdb --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdySynReplyFrame} implementation. + */ +public class DefaultSpdySynReplyFrame extends DefaultSpdyHeaderBlock + implements SpdySynReplyFrame { + + private int streamID; + private boolean last; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + */ + public DefaultSpdySynReplyFrame(int streamID) { + super(); + setStreamID(streamID); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public boolean isLast() { + return last; + } + + public void setLast(boolean last) { + this.last = last; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append("(last: "); + buf.append(isLast()); + buf.append(')'); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Headers:"); + buf.append(StringUtil.NEWLINE); + appendHeaders(buf); + + // Remove the last newline. + buf.setLength(buf.length() - StringUtil.NEWLINE.length()); + return buf.toString(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java new file mode 100644 index 0000000000..6244a60b9e --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdySynStreamFrame} implementation. + */ +public class DefaultSpdySynStreamFrame extends DefaultSpdyHeaderBlock + implements SpdySynStreamFrame { + + private int streamID; + private int associatedToStreamID; + private byte priority; + private boolean last; + private boolean unidirectional; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + * @param associatedToStreamID the Associated-To-Stream-ID of this frame + * @param priority the priority of the stream + */ + public DefaultSpdySynStreamFrame( + int streamID, int associatedToStreamID, byte priority) { + super(); + setStreamID(streamID); + setAssociatedToStreamID(associatedToStreamID); + setPriority(priority); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public int getAssociatedToStreamID() { + return associatedToStreamID; + } + + public void setAssociatedToStreamID(int associatedToStreamID) { + if (associatedToStreamID < 0) { + throw new IllegalArgumentException( + "Associated-To-Stream-ID cannot be negative: " + + associatedToStreamID); + } + this.associatedToStreamID = associatedToStreamID; + } + + public byte getPriority() { + return priority; + } + + public void setPriority(byte priority) { + if (priority < 0 || priority > 3) { + throw new IllegalArgumentException( + "Priortiy must be between 0 and 3 inclusive: " + priority); + } + this.priority = priority; + } + + public boolean isLast() { + return last; + } + + public void setLast(boolean last) { + this.last = last; + } + + public boolean isUnidirectional() { + return unidirectional; + } + + public void setUnidirectional(boolean unidirectional) { + this.unidirectional = unidirectional; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append("(last: "); + buf.append(isLast()); + buf.append("; unidirectional: "); + buf.append(isUnidirectional()); + buf.append(')'); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + if (associatedToStreamID != 0) { + buf.append("--> Associated-To-Stream-ID = "); + buf.append(associatedToStreamID); + buf.append(StringUtil.NEWLINE); + } + buf.append("--> Priority = "); + buf.append(priority); + buf.append(StringUtil.NEWLINE); + buf.append("--> Headers:"); + buf.append(StringUtil.NEWLINE); + appendHeaders(buf); + + // Remove the last newline. + buf.setLength(buf.length() - StringUtil.NEWLINE.length()); + return buf.toString(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java new file mode 100644 index 0000000000..8ba36869f5 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java @@ -0,0 +1,176 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.buffer.ChannelBuffer; + +final class SpdyCodecUtil { + + static final int SPDY_VERSION = 2; + + static final int SPDY_HEADER_TYPE_OFFSET = 2; + static final int SPDY_HEADER_FLAGS_OFFSET = 4; + static final int SPDY_HEADER_LENGTH_OFFSET = 5; + static final int SPDY_HEADER_SIZE = 8; + + static final int SPDY_MAX_LENGTH = 0xFFFFFF; // Length is a 24-bit field + + static final byte SPDY_DATA_FLAG_FIN = 0x01; + static final byte SPDY_DATA_FLAG_COMPRESS = 0x02; + + static final int SPDY_SYN_STREAM_FRAME = 1; + static final int SPDY_SYN_REPLY_FRAME = 2; + static final int SPDY_RST_STREAM_FRAME = 3; + static final int SPDY_SETTINGS_FRAME = 4; + static final int SPDY_NOOP_FRAME = 5; + static final int SPDY_PING_FRAME = 6; + static final int SPDY_GOAWAY_FRAME = 7; + static final int SPDY_HEADERS_FRAME = 8; + static final int SPDY_WINDOW_UPDATE_FRAME = 9; + + static final byte SPDY_FLAG_FIN = 0x01; + static final byte SPDY_FLAG_UNIDIRECTIONAL = 0x02; + + static final byte SPDY_SETTINGS_CLEAR = 0x01; + static final byte SPDY_SETTINGS_PERSIST_VALUE = 0x01; + static final byte SPDY_SETTINGS_PERSISTED = 0x02; + + static final int SPDY_SETTINGS_MAX_ID = 0xFFFFFF; // ID is a 24-bit field + + static final int SPDY_MAX_NV_LENGTH = 0xFFFF; // Length is a 16-bit field + + // Zlib Dictionary + private static final String SPDY_DICT_S = + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" + + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" + + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" + + "-agent10010120020120220320420520630030130230330430530630740040140240340440" + + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" + + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" + + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" + + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" + + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" + + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" + + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" + + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" + + ".1statusversionurl "; + static final byte[] SPDY_DICT; + static { + byte[] SPDY_DICT_ = null; + + try { + SPDY_DICT_ = SPDY_DICT_S.getBytes("US-ASCII"); + // dictionary is null terminated + SPDY_DICT_[SPDY_DICT_.length - 1] = (byte) 0; + } catch (Exception e) { + SPDY_DICT_ = new byte[1]; + } + + SPDY_DICT = SPDY_DICT_; + } + + + private SpdyCodecUtil() { + } + + + /** + * Reads a big-endian unsigned short integer from the buffer. + */ + static int getUnsignedShort(ChannelBuffer buf, int offset) { + return (int) ((buf.getByte(offset) & 0xFF) << 8 | + (buf.getByte(offset + 1) & 0xFF)); + } + + /** + * Reads a big-endian unsigned medium integer from the buffer. + */ + static int getUnsignedMedium(ChannelBuffer buf, int offset) { + return (int) ((buf.getByte(offset) & 0xFF) << 16 | + (buf.getByte(offset + 1) & 0xFF) << 8 | + (buf.getByte(offset + 2) & 0xFF)); + } + + /** + * Reads a big-endian (31-bit) integer from the buffer. + */ + static int getUnsignedInt(ChannelBuffer buf, int offset) { + return (int) ((buf.getByte(offset) & 0x7F) << 24 | + (buf.getByte(offset + 1) & 0xFF) << 16 | + (buf.getByte(offset + 2) & 0xFF) << 8 | + (buf.getByte(offset + 3) & 0xFF)); + } + + /** + * Reads a big-endian signed integer from the buffer. + */ + static int getSignedInt(ChannelBuffer buf, int offset) { + return (int) ((buf.getByte(offset) & 0xFF) << 24 | + (buf.getByte(offset + 1) & 0xFF) << 16 | + (buf.getByte(offset + 2) & 0xFF) << 8 | + (buf.getByte(offset + 3) & 0xFF)); + } + + /** + * Validate a SPDY header name. + */ + static void validateHeaderName(String name) { + if (name == null) { + throw new NullPointerException("name"); + } + if (name.length() == 0) { + throw new IllegalArgumentException( + "name cannot be length zero"); + } + // Since name may only contain ascii characters, for valid names + // name.length() returns the number of bytes when UTF-8 encoded. + if (name.length() > SPDY_MAX_NV_LENGTH) { + throw new IllegalArgumentException( + "name exceeds allowable length: " + name); + } + for (int i = 0; i < name.length(); i ++) { + char c = name.charAt(i); + if (c == 0) { + throw new IllegalArgumentException( + "name contains null character: " + name); + } + if (c > 127) { + throw new IllegalArgumentException( + "name contains non-ascii character: " + name); + } + } + } + + /** + * Validate a SPDY header value. Does not validate max length. + */ + static void validateHeaderValue(String value) { + if (value == null) { + throw new NullPointerException("value"); + } + if (value.length() == 0) { + throw new IllegalArgumentException( + "value cannot be length zero"); + } + for (int i = 0; i < value.length(); i ++) { + char c = value.charAt(i); + if (c == 0) { + throw new IllegalArgumentException( + "value contains null character: " + value); + } + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java new file mode 100644 index 0000000000..a5d4f1b26e --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; + +/** + * A SPDY Protocol Data Frame + */ +public interface SpdyDataFrame { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); + + /** + * Returns {@code true} if this frame is the last frame to be transmitted + * on the stream. + */ + boolean isLast(); + + /** + * Sets if this frame is the last frame to be transmitted on the stream. + */ + void setLast(boolean last); + + /** + * Returns {@code true} if the data in this frame has been compressed. + */ + boolean isCompressed(); + + /** + * Sets if the data in this frame has been compressed. + */ + void setCompressed(boolean compressed); + + /** + * Returns the data payload of this frame. If there is no data payload + * {@link ChannelBuffers#EMPTY_BUFFER} is returned. + */ + ChannelBuffer getData(); + + /** + * Sets the data payload of this frame. If {@code null} is specified, + * the data payload will be set to {@link ChannelBuffers#EMPTY_BUFFER}. + * The data payload cannot exceed 16777215 bytes. + */ + void setData(ChannelBuffer data); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java new file mode 100644 index 0000000000..0850360246 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.channel.ChannelDownstreamHandler; +import io.netty.channel.ChannelEvent; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelUpstreamHandler; + +/** + * A combination of {@link SpdyFrameDecoder} and {@link SpdyFrameEncoder}. + * @apiviz.has io.netty.handler.codec.spdy.SpdyFrameDecoder + * @apiviz.has io.netty.handler.codec.spdy.SpdyFrameEncoder + */ +public class SpdyFrameCodec implements ChannelUpstreamHandler, + ChannelDownstreamHandler { + + private final SpdyFrameDecoder decoder = new SpdyFrameDecoder(); + private final SpdyFrameEncoder encoder = new SpdyFrameEncoder(); + + public SpdyFrameCodec() { + } + + public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) + throws Exception { + decoder.handleUpstream(ctx, e); + } + + public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e) + throws Exception { + encoder.handleDownstream(ctx, e); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java new file mode 100644 index 0000000000..8105404ac2 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java @@ -0,0 +1,322 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import io.netty.buffer.ChannelBuffer; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.compression.ZlibDecoder; +import io.netty.handler.codec.embedder.DecoderEmbedder; +import io.netty.handler.codec.frame.FrameDecoder; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + +/** + * Decodes {@link ChannelBuffer}s into SPDY Data and Control Frames. + */ +public class SpdyFrameDecoder extends FrameDecoder { + + private final DecoderEmbedder headerBlockDecompressor = + new DecoderEmbedder(new ZlibDecoder(SPDY_DICT)); + + public SpdyFrameDecoder() { + super(); + } + + @Override + protected Object decode( + ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) + throws Exception { + + // Must read common header to determine frame length + if (buffer.readableBytes() < SPDY_HEADER_SIZE) { + return null; + } + + // Get frame length from common header + int frameOffset = buffer.readerIndex(); + int lengthOffset = frameOffset + SPDY_HEADER_LENGTH_OFFSET; + int dataLength = getUnsignedMedium(buffer, lengthOffset); + int frameLength = SPDY_HEADER_SIZE + dataLength; + + // Wait until entire frame is readable + if (buffer.readableBytes() < frameLength) { + return null; + } + + // Read common header fields + boolean control = (buffer.getByte(frameOffset) & 0x80) != 0; + int flagsOffset = frameOffset + SPDY_HEADER_FLAGS_OFFSET; + byte flags = buffer.getByte(flagsOffset); + + if (control) { + // Decode control frame common header + int version = getUnsignedShort(buffer, frameOffset) & 0x7FFF; + + // Spdy versioning spec is broken + if (version != SPDY_VERSION) { + buffer.skipBytes(frameLength); + throw new SpdyProtocolException( + "Unsupported version: " + version); + } + + int typeOffset = frameOffset + SPDY_HEADER_TYPE_OFFSET; + int type = getUnsignedShort(buffer, typeOffset); + buffer.skipBytes(SPDY_HEADER_SIZE); + + return decodeControlFrame(type, flags, buffer.readBytes(dataLength)); + } else { + // Decode data frame common header + int streamID = getUnsignedInt(buffer, frameOffset); + buffer.skipBytes(SPDY_HEADER_SIZE); + + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID); + spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); + spdyDataFrame.setCompressed((flags & SPDY_DATA_FLAG_COMPRESS) != 0); + spdyDataFrame.setData(buffer.readBytes(dataLength)); + + return spdyDataFrame; + } + } + + private Object decodeControlFrame(int type, byte flags, ChannelBuffer data) + throws Exception { + int streamID; + boolean last; + + switch (type) { + case SPDY_SYN_STREAM_FRAME: + if (data.readableBytes() < 12) { + throw new SpdyProtocolException( + "Received invalid SYN_STREAM control frame"); + } + streamID = getUnsignedInt(data, data.readerIndex()); + int associatedToStreamID = getUnsignedInt(data, data.readerIndex() + 4); + byte priority = (byte) (data.getByte(data.readerIndex() + 8) >> 6 & 0x03); + data.skipBytes(10); + + SpdySynStreamFrame spdySynStreamFrame = + new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority); + + last = (flags & SPDY_FLAG_FIN) != 0; + boolean unid = (flags & SPDY_FLAG_UNIDIRECTIONAL) != 0; + spdySynStreamFrame.setLast(last); + spdySynStreamFrame.setUnidirectional(unid); + + decodeHeaderBlock(spdySynStreamFrame, decompress(data)); + + return spdySynStreamFrame; + + case SPDY_SYN_REPLY_FRAME: + if (data.readableBytes() < 8) { + throw new SpdyProtocolException( + "Received invalid SYN_REPLY control frame"); + } + streamID = getUnsignedInt(data, data.readerIndex()); + data.skipBytes(6); + + SpdySynReplyFrame spdySynReplyFrame = + new DefaultSpdySynReplyFrame(streamID); + + last = (flags & SPDY_FLAG_FIN) != 0; + spdySynReplyFrame.setLast(last); + + decodeHeaderBlock(spdySynReplyFrame, decompress(data)); + + return spdySynReplyFrame; + + case SPDY_RST_STREAM_FRAME: + if (flags != 0 || data.readableBytes() != 8) { + throw new SpdyProtocolException( + "Received invalid RST_STREAM control frame"); + } + streamID = getUnsignedInt(data, data.readerIndex()); + int statusCode = getSignedInt(data, data.readerIndex() + 4); + if (statusCode == 0) { + throw new SpdyProtocolException( + "Received invalid RST_STREAM status code"); + } + + return new DefaultSpdyRstStreamFrame(streamID, statusCode); + + case SPDY_SETTINGS_FRAME: + if (data.readableBytes() < 4) { + throw new SpdyProtocolException( + "Received invalid SETTINGS control frame"); + } + // Each ID/Value entry is 8 bytes + // The number of entries cannot exceed SPDY_MAX_LENGTH / 8; + int numEntries = getUnsignedInt(data, data.readerIndex()); + if ((numEntries > (SPDY_MAX_LENGTH - 4) / 8) || + (data.readableBytes() != numEntries * 8 + 4)) { + throw new SpdyProtocolException( + "Received invalid SETTINGS control frame"); + } + data.skipBytes(4); + + SpdySettingsFrame spdySettingsFrame = new DefaultSpdySettingsFrame(); + + boolean clear = (flags & SPDY_SETTINGS_CLEAR) != 0; + spdySettingsFrame.setClearPreviouslyPersistedSettings(clear); + + for (int i = 0; i < numEntries; i ++) { + // Chromium Issue 79156 + // SPDY setting ids are not written in network byte order + // Read id assuming the architecture is little endian + int ID = (data.readByte() & 0xFF) | + (data.readByte() & 0xFF) << 8 | + (data.readByte() & 0xFF) << 16; + byte ID_flags = data.readByte(); + int value = getSignedInt(data, data.readerIndex()); + data.skipBytes(4); + + if (!(spdySettingsFrame.isSet(ID))) { + boolean persistVal = (ID_flags & SPDY_SETTINGS_PERSIST_VALUE) != 0; + boolean persisted = (ID_flags & SPDY_SETTINGS_PERSISTED) != 0; + spdySettingsFrame.setValue(ID, value, persistVal, persisted); + } + } + + return spdySettingsFrame; + + case SPDY_NOOP_FRAME: + if (data.readableBytes() != 0) { + throw new SpdyProtocolException( + "Received invalid NOOP control frame"); + } + + return null; + + case SPDY_PING_FRAME: + if (data.readableBytes() != 4) { + throw new SpdyProtocolException( + "Received invalid PING control frame"); + } + int ID = getSignedInt(data, data.readerIndex()); + + return new DefaultSpdyPingFrame(ID); + + case SPDY_GOAWAY_FRAME: + if (data.readableBytes() != 4) { + throw new SpdyProtocolException( + "Received invalid GOAWAY control frame"); + } + int lastGoodStreamID = getUnsignedInt(data, data.readerIndex()); + + return new DefaultSpdyGoAwayFrame(lastGoodStreamID); + + case SPDY_HEADERS_FRAME: + // Protocol allows length 4 frame when there are no name/value pairs + if (data.readableBytes() == 4) { + streamID = getUnsignedInt(data, data.readerIndex()); + return new DefaultSpdyHeadersFrame(streamID); + } + + if (data.readableBytes() < 8) { + throw new SpdyProtocolException( + "Received invalid HEADERS control frame"); + } + streamID = getUnsignedInt(data, data.readerIndex()); + data.skipBytes(6); + + SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamID); + + decodeHeaderBlock(spdyHeadersFrame, decompress(data)); + + return spdyHeadersFrame; + + case SPDY_WINDOW_UPDATE_FRAME: + return null; + + default: + return null; + } + } + + private ChannelBuffer decompress(ChannelBuffer compressed) throws Exception { + if ((compressed.readableBytes() == 2) && + (compressed.getShort(compressed.readerIndex()) == 0)) { + return compressed; + } + headerBlockDecompressor.offer(compressed); + return headerBlockDecompressor.poll(); + } + + private void decodeHeaderBlock(SpdyHeaderBlock headerFrame, ChannelBuffer headerBlock) + throws Exception { + if (headerBlock.readableBytes() < 2) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + int numEntries = getUnsignedShort(headerBlock, headerBlock.readerIndex()); + headerBlock.skipBytes(2); + for (int i = 0; i < numEntries; i ++) { + if (headerBlock.readableBytes() < 2) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + int nameLength = getUnsignedShort(headerBlock, headerBlock.readerIndex()); + headerBlock.skipBytes(2); + if (nameLength == 0) { + headerFrame.setInvalid(); + return; + } + if (headerBlock.readableBytes() < nameLength) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + byte[] nameBytes = new byte[nameLength]; + headerBlock.readBytes(nameBytes); + String name = new String(nameBytes, "UTF-8"); + if (headerFrame.containsHeader(name)) { + throw new SpdyProtocolException( + "Received duplicate header name: " + name); + } + if (headerBlock.readableBytes() < 2) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + int valueLength = getUnsignedShort(headerBlock, headerBlock.readerIndex()); + headerBlock.skipBytes(2); + if (valueLength == 0) { + headerFrame.setInvalid(); + return; + } + if (headerBlock.readableBytes() < valueLength) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + byte[] valueBytes = new byte[valueLength]; + headerBlock.readBytes(valueBytes); + int index = 0; + int offset = 0; + while (index < valueBytes.length) { + while (index < valueBytes.length && valueBytes[index] != (byte) 0) { + index ++; + } + if (index < valueBytes.length && valueBytes[index + 1] == (byte) 0) { + // Received multiple, in-sequence NULL characters + headerFrame.setInvalid(); + return; + } + String value = new String(valueBytes, offset, index - offset, "UTF-8"); + headerFrame.addHeader(name, value); + index ++; + offset = index; + } + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java new file mode 100644 index 0000000000..dd08b46c4a --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java @@ -0,0 +1,257 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.nio.ByteOrder; +import java.util.Set; + +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.compression.ZlibEncoder; +import io.netty.handler.codec.embedder.EncoderEmbedder; +import io.netty.handler.codec.oneone.OneToOneEncoder; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + +/** + * Encodes a SPDY Data or Control Frame into a {@link ChannelBuffer}. + */ +public class SpdyFrameEncoder extends OneToOneEncoder { + + private final EncoderEmbedder headerBlockCompressor = + new EncoderEmbedder(new ZlibEncoder(9, SPDY_DICT)); + + public SpdyFrameEncoder() { + super(); + } + + @Override + protected Object encode( + ChannelHandlerContext ctx, Channel channel, Object msg) + throws Exception { + + if (msg instanceof SpdyDataFrame) { + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + ChannelBuffer data = spdyDataFrame.getData(); + byte flags = spdyDataFrame.isLast() ? SPDY_DATA_FLAG_FIN : 0; + if (spdyDataFrame.isCompressed()) { + flags |= SPDY_DATA_FLAG_COMPRESS; + } + ChannelBuffer header = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE); + header.writeInt(spdyDataFrame.getStreamID() & 0x7FFFFFFF); + header.writeByte(flags); + header.writeMedium(data.readableBytes()); + return ChannelBuffers.wrappedBuffer(header, data); + + } else if (msg instanceof SpdySynStreamFrame) { + + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(spdySynStreamFrame)); + byte flags = spdySynStreamFrame.isLast() ? SPDY_FLAG_FIN : 0; + if (spdySynStreamFrame.isUnidirectional()) { + flags |= SPDY_FLAG_UNIDIRECTIONAL; + } + int headerBlockLength = data.readableBytes(); + int length = (headerBlockLength == 0) ? 12 : 10 + headerBlockLength; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_SYN_STREAM_FRAME); + frame.writeByte(flags); + frame.writeMedium(length); + frame.writeInt(spdySynStreamFrame.getStreamID()); + frame.writeInt(spdySynStreamFrame.getAssociatedToStreamID()); + frame.writeShort(((short) spdySynStreamFrame.getPriority()) << 14); + if (data.readableBytes() == 0) { + frame.writeShort(0); + } + return ChannelBuffers.wrappedBuffer(frame, data); + + } else if (msg instanceof SpdySynReplyFrame) { + + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(spdySynReplyFrame)); + byte flags = spdySynReplyFrame.isLast() ? SPDY_FLAG_FIN : 0; + int headerBlockLength = data.readableBytes(); + int length = (headerBlockLength == 0) ? 8 : 6 + headerBlockLength; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_SYN_REPLY_FRAME); + frame.writeByte(flags); + frame.writeMedium(length); + frame.writeInt(spdySynReplyFrame.getStreamID()); + if (data.readableBytes() == 0) { + frame.writeInt(0); + } else { + frame.writeShort(0); + } + return ChannelBuffers.wrappedBuffer(frame, data); + + } else if (msg instanceof SpdyRstStreamFrame) { + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 8); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_RST_STREAM_FRAME); + frame.writeInt(8); + frame.writeInt(spdyRstStreamFrame.getStreamID()); + frame.writeInt(spdyRstStreamFrame.getStatus().getCode()); + return frame; + + } else if (msg instanceof SpdySettingsFrame) { + + SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; + byte flags = spdySettingsFrame.clearPreviouslyPersistedSettings() ? + SPDY_SETTINGS_CLEAR : 0; + Set IDs = spdySettingsFrame.getIDs(); + int numEntries = IDs.size(); + int length = 4 + numEntries * 8; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_SETTINGS_FRAME); + frame.writeByte(flags); + frame.writeMedium(length); + frame.writeInt(numEntries); + for (Integer ID: IDs) { + int id = ID.intValue(); + byte ID_flags = (byte) 0; + if (spdySettingsFrame.persistValue(id)) { + ID_flags |= SPDY_SETTINGS_PERSIST_VALUE; + } + if (spdySettingsFrame.isPersisted(id)) { + ID_flags |= SPDY_SETTINGS_PERSISTED; + } + // Chromium Issue 79156 + // SPDY setting ids are not written in network byte order + // Write id assuming the architecture is little endian + frame.writeByte((id >> 0) & 0xFF); + frame.writeByte((id >> 8) & 0xFF); + frame.writeByte((id >> 16) & 0xFF); + frame.writeByte(ID_flags); + frame.writeInt(spdySettingsFrame.getValue(id)); + } + return frame; + + } else if (msg instanceof SpdyNoOpFrame) { + + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_NOOP_FRAME); + frame.writeInt(0); + return frame; + + } else if (msg instanceof SpdyPingFrame) { + + SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 4); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_PING_FRAME); + frame.writeInt(4); + frame.writeInt(spdyPingFrame.getID()); + return frame; + + } else if (msg instanceof SpdyGoAwayFrame) { + + SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 4); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_GOAWAY_FRAME); + frame.writeInt(4); + frame.writeInt(spdyGoAwayFrame.getLastGoodStreamID()); + return frame; + + } else if (msg instanceof SpdyHeadersFrame) { + + SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(spdyHeadersFrame)); + int headerBlockLength = data.readableBytes(); + int length = (headerBlockLength == 0) ? 4 : 6 + headerBlockLength; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_HEADERS_FRAME); + frame.writeInt(length); + frame.writeInt(spdyHeadersFrame.getStreamID()); + if (data.readableBytes() != 0) { + frame.writeShort(0); + } + return ChannelBuffers.wrappedBuffer(frame, data); + } + + // Unknown message type + return msg; + } + + private ChannelBuffer encodeHeaderBlock(SpdyHeaderBlock headerFrame) + throws Exception { + Set names = headerFrame.getHeaderNames(); + int numHeaders = names.size(); + if (numHeaders == 0) { + return ChannelBuffers.EMPTY_BUFFER; + } + if (numHeaders > SPDY_MAX_NV_LENGTH) { + throw new IllegalArgumentException( + "header block contains too many headers"); + } + ChannelBuffer headerBlock = ChannelBuffers.dynamicBuffer( + ByteOrder.BIG_ENDIAN, 256); + headerBlock.writeShort(numHeaders); + for (String name: names) { + byte[] nameBytes = name.getBytes("UTF-8"); + headerBlock.writeShort(nameBytes.length); + headerBlock.writeBytes(nameBytes); + int savedIndex = headerBlock.writerIndex(); + int valueLength = 0; + headerBlock.writeShort(valueLength); + for (String value: headerFrame.getHeaders(name)) { + byte[] valueBytes = value.getBytes("UTF-8"); + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0); + valueLength += valueBytes.length + 1; + } + valueLength --; + if (valueLength > SPDY_MAX_NV_LENGTH) { + throw new IllegalArgumentException( + "header exceeds allowable length: " + name); + } + headerBlock.setShort(savedIndex, valueLength); + headerBlock.writerIndex(headerBlock.writerIndex() - 1); + } + return headerBlock; + } + + private synchronized ChannelBuffer compressHeaderBlock( + ChannelBuffer uncompressed) throws Exception { + if (uncompressed.readableBytes() == 0) { + return ChannelBuffers.EMPTY_BUFFER; + } + headerBlockCompressor.offer(uncompressed); + return headerBlockCompressor.poll(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java new file mode 100644 index 0000000000..61785e8cf5 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * A SPDY Protocol GOAWAY Control Frame + */ +public interface SpdyGoAwayFrame { + + /** + * Returns the Last-good-stream-ID of this frame. + */ + int getLastGoodStreamID(); + + /** + * Sets the Last-good-stream-ID of this frame. The Last-good-stream-ID + * cannot be negative. + */ + void setLastGoodStreamID(int lastGoodStreamID); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlock.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlock.java new file mode 100644 index 0000000000..31139c4658 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlock.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A SPDY Name/Value Header Block which provides common properties for + * {@link SpdySynStreamFrame}, {@link SpdySynReplyFrame}, and + * {@link SpdyHeadersFrame}. + * @see SpdyHeaders + */ +public interface SpdyHeaderBlock { + + /** + * Returns {@code true} if this header block is invalid. + * A RST_STREAM frame with code PROTOCOL_ERROR should be sent. + */ + boolean isInvalid(); + + /** + * Marks this header block as invalid. + */ + void setInvalid(); + + /** + * Returns the header value with the specified header name. If there is + * more than one header value for the specified header name, the first + * value is returned. + * + * @return the header value or {@code null} if there is no such header + */ + String getHeader(String name); + + /** + * Returns the header values with the specified header name. + * + * @return the {@link List} of header values. An empty list if there is no + * such header. + */ + List getHeaders(String name); + + /** + * Returns all header names and values that this block contains. + * + * @return the {@link List} of the header name-value pairs. An empty list + * if there is no header in this message. + */ + List> getHeaders(); + + /** + * Returns {@code true} if and only if there is a header with the specified + * header name. + */ + boolean containsHeader(String name); + + /** + * Returns the {@link Set} of all header names that this block contains. + */ + Set getHeaderNames(); + + /** + * Adds a new header with the specified name and value. + */ + void addHeader(String name, Object value); + + /** + * Sets a new header with the specified name and value. If there is an + * existing header with the same name, the existing header is removed. + */ + void setHeader(String name, Object value); + + /** + * Sets a new header with the specified name and values. If there is an + * existing header with the same name, the existing header is removed. + */ + void setHeader(String name, Iterable values); + + /** + * Removes the header with the specified name. + */ + void removeHeader(String name); + + /** + * Removes all headers from this block. + */ + void clearHeaders(); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java new file mode 100644 index 0000000000..6197f47c70 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java @@ -0,0 +1,530 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +/** + * Provides the constants for the standard SPDY HTTP header names and commonly + * used utility methods that access an {@link SpdyHeaderBlock}. + * @apiviz.sterotype static + */ +public class SpdyHeaders { + + /** + * SPDY HTTP header names + * @apiviz.sterotype static + */ + public static final class HttpNames { + /** + * {@code "method"} + */ + public static final String METHOD = "method"; + /** + * {@code "scheme"} + */ + public static final String SCHEME = "scheme"; + /** + * {@code "status"} + */ + public static final String STATUS = "status"; + /** + * {@code "url"} + */ + public static final String URL = "url"; + /** + * {@code "version"} + */ + public static final String VERSION = "version"; + + private HttpNames() { + super(); + } + } + + + /** + * Returns the header value with the specified header name. If there are + * more than one header value for the specified header name, the first + * value is returned. + * + * @return the header value or {@code null} if there is no such header + */ + public static String getHeader(SpdyHeaderBlock block, String name) { + return block.getHeader(name); + } + + /** + * Returns the header value with the specified header name. If there are + * more than one header value for the specified header name, the first + * value is returned. + * + * @return the header value or the {@code defaultValue} if there is no such + * header + */ + public static String getHeader(SpdyHeaderBlock block, String name, String defaultValue) { + String value = block.getHeader(name); + if (value == null) { + return defaultValue; + } + return value; + } + + /** + * Sets a new header with the specified name and value. If there is an + * existing header with the same name, the existing header is removed. + */ + public static void setHeader(SpdyHeaderBlock block, String name, Object value) { + block.setHeader(name, value); + } + + /** + * Sets a new header with the specified name and values. If there is an + * existing header with the same name, the existing header is removed. + */ + public static void setHeader(SpdyHeaderBlock block, String name, Iterable values) { + block.setHeader(name, values); + } + + /** + * Adds a new header with the specified name and value. + */ + public static void addHeader(SpdyHeaderBlock block, String name, Object value) { + block.addHeader(name, value); + } + + /** + * Removes the {@code "method"} header. + */ + public static void removeMethod(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.METHOD); + } + + /** + * Returns the {@link HttpMethod} represented by the {@code "method"} header. + */ + public static HttpMethod getMethod(SpdyHeaderBlock block) { + try { + return HttpMethod.valueOf(block.getHeader(HttpNames.METHOD)); + } catch (Exception e) { + return null; + } + } + + /** + * Sets the {@code "method"} header. + */ + public static void setMethod(SpdyHeaderBlock block, HttpMethod method) { + block.setHeader(HttpNames.METHOD, method.getName()); + } + + /** + * Removes the {@code "scheme"} header. + */ + public static void removeScheme(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.SCHEME); + } + + /** + * Returns the value of the {@code "scheme"} header. + */ + public static String getScheme(SpdyHeaderBlock block) { + return block.getHeader(HttpNames.SCHEME); + } + + /** + * Sets the {@code "scheme"} header. + */ + public static void setScheme(SpdyHeaderBlock block, String value) { + block.setHeader(HttpNames.SCHEME, value); + } + + /** + * Removes the {@code "status"} header. + */ + public static void removeStatus(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.STATUS); + } + + /** + * Returns the {@link HttpResponseStatus} represented by the {@code "status"} header. + */ + public static HttpResponseStatus getStatus(SpdyHeaderBlock block) { + try { + String status = block.getHeader(HttpNames.STATUS); + int space = status.indexOf(' '); + if (space == -1) { + return HttpResponseStatus.valueOf(Integer.parseInt(status)); + } else { + int code = Integer.parseInt(status.substring(0, space)); + String reasonPhrase = status.substring(space + 1); + HttpResponseStatus responseStatus = HttpResponseStatus.valueOf(code); + if (responseStatus.getReasonPhrase().equals(responseStatus)) { + return responseStatus; + } else { + return new HttpResponseStatus(code, reasonPhrase); + } + } + } catch (Exception e) { + return null; + } + } + + /** + * Sets the {@code "status"} header. + */ + public static void setStatus(SpdyHeaderBlock block, HttpResponseStatus status) { + block.setHeader(HttpNames.STATUS, status.toString()); + } + + /** + * Removes the {@code "url"} header. + */ + public static void removeUrl(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.URL); + } + + /** + * Returns the value of the {@code "url"} header. + */ + public static String getUrl(SpdyHeaderBlock block) { + return block.getHeader(HttpNames.URL); + } + + /** + * Sets the {@code "url"} header. + */ + public static void setUrl(SpdyHeaderBlock block, String value) { + block.setHeader(HttpNames.URL, value); + } + + /** + * Removes the {@code "version"} header. + */ + public static void removeVersion(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.VERSION); + } + + /** + * Returns the {@link HttpVersion} represented by the {@code "version"} header. + */ + public static HttpVersion getVersion(SpdyHeaderBlock block) { + try { + return HttpVersion.valueOf(block.getHeader(HttpNames.VERSION)); + } catch (Exception e) { + return null; + } + } + + /** + * Sets the {@code "version"} header. + */ + public static void setVersion(SpdyHeaderBlock block, HttpVersion version) { + block.setHeader(HttpNames.VERSION, version.getText()); + } + + + private static final int BUCKET_SIZE = 17; + + private static int hash(String name) { + int h = 0; + for (int i = name.length() - 1; i >= 0; i --) { + char c = name.charAt(i); + if (c >= 'A' && c <= 'Z') { + c += 32; + } + h = 31 * h + c; + } + + if (h > 0) { + return h; + } else if (h == Integer.MIN_VALUE) { + return Integer.MAX_VALUE; + } else { + return -h; + } + } + + private static boolean eq(String name1, String name2) { + int nameLen = name1.length(); + if (nameLen != name2.length()) { + return false; + } + + for (int i = nameLen - 1; i >= 0; i --) { + char c1 = name1.charAt(i); + char c2 = name2.charAt(i); + if (c1 != c2) { + if (c1 >= 'A' && c1 <= 'Z') { + c1 += 32; + } + if (c2 >= 'A' && c2 <= 'Z') { + c2 += 32; + } + if (c1 != c2) { + return false; + } + } + } + return true; + } + + private static int index(int hash) { + return hash % BUCKET_SIZE; + } + + private final Entry[] entries = new Entry[BUCKET_SIZE]; + private final Entry head = new Entry(-1, null, null); + + SpdyHeaders() { + head.before = head.after = head; + } + + void addHeader(final String name, final Object value) { + String lowerCaseName = name.toLowerCase(); + SpdyCodecUtil.validateHeaderName(lowerCaseName); + String strVal = toString(value); + SpdyCodecUtil.validateHeaderValue(strVal); + int h = hash(lowerCaseName); + int i = index(h); + addHeader0(h, i, lowerCaseName, strVal); + } + + private void addHeader0(int h, int i, final String name, final String value) { + // Update the hash table. + Entry e = entries[i]; + Entry newEntry; + entries[i] = newEntry = new Entry(h, name, value); + newEntry.next = e; + + // Update the linked list. + newEntry.addBefore(head); + } + + void removeHeader(final String name) { + if (name == null) { + throw new NullPointerException("name"); + } + String lowerCaseName = name.toLowerCase(); + int h = hash(lowerCaseName); + int i = index(h); + removeHeader0(h, i, lowerCaseName); + } + + private void removeHeader0(int h, int i, String name) { + Entry e = entries[i]; + if (e == null) { + return; + } + + for (;;) { + if (e.hash == h && eq(name, e.key)) { + e.remove(); + Entry next = e.next; + if (next != null) { + entries[i] = next; + e = next; + } else { + entries[i] = null; + return; + } + } else { + break; + } + } + + for (;;) { + Entry next = e.next; + if (next == null) { + break; + } + if (next.hash == h && eq(name, next.key)) { + e.next = next.next; + next.remove(); + } else { + e = next; + } + } + } + + void setHeader(final String name, final Object value) { + String lowerCaseName = name.toLowerCase(); + SpdyCodecUtil.validateHeaderName(lowerCaseName); + String strVal = toString(value); + SpdyCodecUtil.validateHeaderValue(strVal); + int h = hash(lowerCaseName); + int i = index(h); + removeHeader0(h, i, lowerCaseName); + addHeader0(h, i, lowerCaseName, strVal); + } + + void setHeader(final String name, final Iterable values) { + if (values == null) { + throw new NullPointerException("values"); + } + + String lowerCaseName = name.toLowerCase(); + SpdyCodecUtil.validateHeaderName(lowerCaseName); + + int h = hash(lowerCaseName); + int i = index(h); + + removeHeader0(h, i, lowerCaseName); + for (Object v: values) { + if (v == null) { + break; + } + String strVal = toString(v); + SpdyCodecUtil.validateHeaderValue(strVal); + addHeader0(h, i, lowerCaseName, strVal); + } + } + + void clearHeaders() { + for (int i = 0; i < entries.length; i ++) { + entries[i] = null; + } + head.before = head.after = head; + } + + String getHeader(final String name) { + if (name == null) { + throw new NullPointerException("name"); + } + + int h = hash(name); + int i = index(h); + Entry e = entries[i]; + while (e != null) { + if (e.hash == h && eq(name, e.key)) { + return e.value; + } + + e = e.next; + } + return null; + } + + List getHeaders(final String name) { + if (name == null) { + throw new NullPointerException("name"); + } + + LinkedList values = new LinkedList(); + + int h = hash(name); + int i = index(h); + Entry e = entries[i]; + while (e != null) { + if (e.hash == h && eq(name, e.key)) { + values.addFirst(e.value); + } + e = e.next; + } + return values; + } + + List> getHeaders() { + List> all = + new LinkedList>(); + + Entry e = head.after; + while (e != head) { + all.add(e); + e = e.after; + } + return all; + } + + boolean containsHeader(String name) { + return getHeader(name) != null; + } + + Set getHeaderNames() { + Set names = new TreeSet(); + + Entry e = head.after; + while (e != head) { + names.add(e.key); + e = e.after; + } + return names; + } + + private static String toString(Object value) { + if (value == null) { + return null; + } + return value.toString(); + } + + private static final class Entry implements Map.Entry { + final int hash; + final String key; + String value; + Entry next; + Entry before, after; + + Entry(int hash, String key, String value) { + this.hash = hash; + this.key = key; + this.value = value; + } + + void remove() { + before.after = after; + after.before = before; + } + + void addBefore(Entry e) { + after = e; + before = e.before; + before.after = this; + after.before = this; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public String setValue(String value) { + if (value == null) { + throw new NullPointerException("value"); + } + SpdyCodecUtil.validateHeaderValue(value); + String oldValue = this.value; + this.value = value; + return oldValue; + } + + @Override + public String toString() { + return key + "=" + value; + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java new file mode 100644 index 0000000000..69336025ff --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * A SPDY Protocol HEADERS Control Frame + */ +public interface SpdyHeadersFrame extends SpdyHeaderBlock { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java new file mode 100644 index 0000000000..d3abe5b7f8 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * A SPDY Protocol NOOP Control Frame + */ +public interface SpdyNoOpFrame { +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyPingFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyPingFrame.java new file mode 100644 index 0000000000..739d771643 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyPingFrame.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * A SPDY Protocol PING Control Frame + */ +public interface SpdyPingFrame { + + /** + * Returns the ID of this frame. + */ + int getID(); + + /** + * Sets the ID of this frame. + */ + void setID(int ID); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java new file mode 100644 index 0000000000..7f4d9a3e67 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * An {@link Exception} which is thrown when the received frame cannot + * be decoded by the {@link SpdyFrameDecoder}. + * @apiviz.exclude + */ +public class SpdyProtocolException extends Exception { + + /** + * Creates a new instance. + */ + public SpdyProtocolException() { + super(); + } + + /** + * Creates a new instance. + */ + public SpdyProtocolException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new instance. + */ + public SpdyProtocolException(String message) { + super(message); + } + + /** + * Creates a new instance. + */ + public SpdyProtocolException(Throwable cause) { + super(cause); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyRstStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyRstStreamFrame.java new file mode 100644 index 0000000000..688830b5ea --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyRstStreamFrame.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * A SPDY Protocol RST_STREAM Control Frame + */ +public interface SpdyRstStreamFrame { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); + + /** + * Returns the status of this frame. + */ + SpdyStreamStatus getStatus(); + + /** + * Sets the status of this frame. + */ + void setStatus(SpdyStreamStatus status); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySession.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySession.java new file mode 100644 index 0000000000..1487391ac8 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySession.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +final class SpdySession { + + private final Map activeStreams = + new ConcurrentHashMap(); + + SpdySession() { + } + + public int numActiveStreams() { + return activeStreams.size(); + } + + public boolean noActiveStreams() { + return activeStreams.isEmpty(); + } + + public boolean isActiveStream(int streamID) { + return activeStreams.containsKey(new Integer(streamID)); + } + + public void acceptStream(int streamID, boolean remoteSideClosed, boolean localSideClosed) { + if (!remoteSideClosed || !localSideClosed) { + activeStreams.put(new Integer(streamID), + new StreamState(remoteSideClosed, localSideClosed)); + } + return; + } + + public void removeStream(int streamID) { + activeStreams.remove(new Integer(streamID)); + return; + } + + public boolean isRemoteSideClosed(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return (state == null) || state.isRemoteSideClosed(); + } + + public void closeRemoteSide(int streamID) { + Integer StreamID = new Integer(streamID); + StreamState state = activeStreams.get(StreamID); + if (state != null) { + state.closeRemoteSide(); + if (state.isLocalSideClosed()) { + activeStreams.remove(StreamID); + } + } + } + + public boolean isLocalSideClosed(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return (state == null) || state.isLocalSideClosed(); + } + + public void closeLocalSide(int streamID) { + Integer StreamID = new Integer(streamID); + StreamState state = activeStreams.get(StreamID); + if (state != null) { + state.closeLocalSide(); + if (state.isRemoteSideClosed()) { + activeStreams.remove(StreamID); + } + } + } + + public boolean hasReceivedReply(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return (state != null) && state.hasReceivedReply(); + } + + public void receivedReply(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + if (state != null) { + state.receivedReply(); + } + } + + private static final class StreamState { + + private boolean remoteSideClosed; + private boolean localSideClosed; + private boolean receivedReply; + + public StreamState(boolean remoteSideClosed, boolean localSideClosed) { + this.remoteSideClosed = remoteSideClosed; + this.localSideClosed = localSideClosed; + } + + public boolean isRemoteSideClosed() { + return remoteSideClosed; + } + + public void closeRemoteSide() { + remoteSideClosed = true; + } + + public boolean isLocalSideClosed() { + return localSideClosed; + } + + public void closeLocalSide() { + localSideClosed = true; + } + + public boolean hasReceivedReply() { + return receivedReply; + } + + public void receivedReply() { + receivedReply = true; + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java new file mode 100644 index 0000000000..d4a791156b --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java @@ -0,0 +1,497 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.atomic.AtomicInteger; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelDownstreamHandler; +import io.netty.channel.ChannelEvent; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelStateEvent; +import io.netty.channel.Channels; +import io.netty.channel.MessageEvent; +import io.netty.channel.SimpleChannelUpstreamHandler; + +/** + * Manages streams within a SPDY session. + */ +public class SpdySessionHandler extends SimpleChannelUpstreamHandler + implements ChannelDownstreamHandler { + + private static final SpdyProtocolException PROTOCOL_EXCEPTION = new SpdyProtocolException(); + + private final SpdySession spdySession = new SpdySession(); + private volatile int lastGoodStreamID; + + private volatile int remoteConcurrentStreams; + private volatile int localConcurrentStreams; + private volatile int maxConcurrentStreams; + + private final AtomicInteger pings = new AtomicInteger(); + + private volatile boolean sentGoAwayFrame; + private volatile boolean receivedGoAwayFrame; + + private volatile ChannelFuture closeSessionFuture; + + private final boolean server; + + /** + * Creates a new session handler. + * + * @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(boolean server) { + super(); + this.server = server; + } + + @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 does not exist, + * it must return a RST_STREAM with 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 receiver should + * close the connection immediately. + * + * If an endpoint receives multiple data frames for invalid Stream-IDs, + * it may terminate the session. + * + * If an endpoint refuses a stream it must ignore any data frames for that stream. + * + * If an endpoint receives data on a stream which has already been torn down, + * it must ignore the data received after the teardown. + */ + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + int streamID = spdyDataFrame.getStreamID(); + + // Check if we received a data frame for a Stream-ID which is not open + if (spdySession.isRemoteSideClosed(streamID)) { + if (!sentGoAwayFrame) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.INVALID_STREAM); + } + return; + } + + // Check if we received a data frame before receiving a SYN_REPLY + if (!isRemoteInitiatedID(streamID) && !spdySession.hasReceivedReply(streamID)) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + return; + } + + if (spdyDataFrame.isLast()) { + // Close remote side of stream + halfCloseStream(streamID, true); + } + + } else if (msg instanceof SpdySynStreamFrame) { + + /* + * SPDY SYN_STREAM frame processing requirements: + * + * If an endpoint receives a SYN_STREAM with a Stream-ID that is not monotonically + * increasing, 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. + */ + + 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, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + return; + } + + // Stream-IDs must be monotonically increassing + if (streamID < lastGoodStreamID) { + issueSessionError(ctx, e.getChannel(), e.getRemoteAddress()); + return; + } + + // Try to accept the stream + boolean remoteSideClosed = spdySynStreamFrame.isLast(); + boolean localSideClosed = spdySynStreamFrame.isUnidirectional(); + if (!acceptStream(streamID, remoteSideClosed, localSideClosed)) { + issueStreamError(ctx, e, 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 PROTOCOL_ERROR. + */ + + 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, streamID, SpdyStreamStatus.INVALID_STREAM); + return; + } + + // Check if we have received multiple frames for the same Stream-ID + if (spdySession.hasReceivedReply(streamID)) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + return; + } + + spdySession.receivedReply(streamID); + if (spdySynReplyFrame.isLast()) { + // Close remote side of stream + halfCloseStream(streamID, true); + } + + } 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. + */ + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + removeStream(spdyRstStreamFrame.getStreamID()); + + } else if (msg instanceof SpdySettingsFrame) { + + /* + * Only concerned with MAX_CONCURRENT_STREAMS + */ + + SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; + updateConcurrentStreams(spdySettingsFrame, true); + + } 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 inforced + 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, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + return; + } + + if (spdySession.isRemoteSideClosed(streamID)) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.INVALID_STREAM); + return; + } + } + + super.messageReceived(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: + 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; + int streamID = spdyDataFrame.getStreamID(); + + if (spdySession.isLocalSideClosed(streamID)) { + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + } + + if (spdyDataFrame.isLast()) { + halfCloseStream(streamID, false); + } + + } else if (msg instanceof SpdySynStreamFrame) { + + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + boolean remoteSideClosed = spdySynStreamFrame.isUnidirectional(); + boolean localSideClosed = spdySynStreamFrame.isLast(); + if (!acceptStream(spdySynStreamFrame.getStreamID(), remoteSideClosed, localSideClosed)) { + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + } + + } else if (msg instanceof SpdySynReplyFrame) { + + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + int streamID = spdySynReplyFrame.getStreamID(); + + if (!isRemoteInitiatedID(streamID) || spdySession.isLocalSideClosed(streamID)) { + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + } + + if (spdySynReplyFrame.isLast()) { + halfCloseStream(streamID, false); + } + + } else if (msg instanceof SpdyRstStreamFrame) { + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + removeStream(spdyRstStreamFrame.getStreamID()); + + } else if (msg instanceof SpdySettingsFrame) { + + SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; + updateConcurrentStreams(spdySettingsFrame, false); + + } 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) { + + // Should send a CLOSE ChannelStateEvent + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + + } else if (msg instanceof SpdyHeadersFrame) { + + SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; + int streamID = spdyHeadersFrame.getStreamID(); + + if (spdySession.isLocalSideClosed(streamID)) { + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + } + } + + ctx.sendDownstream(evt); + } + + /* + * Error Handling + */ + + private void issueSessionError( + ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress) { + + ChannelFuture future = sendGoAwayFrame(ctx, channel, remoteAddress); + future.addListener(ChannelFutureListener.CLOSE); + } + + // Send a RST_STREAM frame in response to an incoming MessageEvent + // Only called in the upstream direction + private void issueStreamError( + ChannelHandlerContext ctx, MessageEvent e, int streamID, SpdyStreamStatus status) { + + removeStream(streamID); + SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamID, status); + Channels.write(ctx, Channels.future(e.getChannel()), spdyRstStreamFrame, e.getRemoteAddress()); + } + + /* + * Helper functions + */ + + private boolean isServerID(int ID) { + return ID % 2 == 0; + } + + private boolean isRemoteInitiatedID(int ID) { + boolean serverID = isServerID(ID); + return (server && !serverID) || (!server && serverID); + } + + private synchronized void updateConcurrentStreams(SpdySettingsFrame settings, boolean remote) { + int newConcurrentStreams = settings.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS); + if (remote) { + remoteConcurrentStreams = newConcurrentStreams; + } else { + localConcurrentStreams = newConcurrentStreams; + } + if (localConcurrentStreams == remoteConcurrentStreams) { + maxConcurrentStreams = localConcurrentStreams; + return; + } + if (localConcurrentStreams == 0) { + maxConcurrentStreams = remoteConcurrentStreams; + return; + } + if (remoteConcurrentStreams == 0) { + maxConcurrentStreams = localConcurrentStreams; + return; + } + if (localConcurrentStreams > remoteConcurrentStreams) { + maxConcurrentStreams = remoteConcurrentStreams; + } else { + maxConcurrentStreams = localConcurrentStreams; + } + } + + // need to synchronize accesses to sentGoAwayFrame and lastGoodStreamID + private synchronized boolean acceptStream( + int streamID, boolean remoteSideClosed, boolean localSideClosed) { + // Cannot initiate any new streams after receiving or sending GOAWAY + if (receivedGoAwayFrame || sentGoAwayFrame) { + return false; + } + if ((maxConcurrentStreams != 0) && + (spdySession.numActiveStreams() >= maxConcurrentStreams)) { + return false; + } + spdySession.acceptStream(streamID, remoteSideClosed, localSideClosed); + if (isRemoteInitiatedID(streamID)) { + lastGoodStreamID = streamID; + } + return true; + } + + private void halfCloseStream(int streamID, boolean remote) { + if (remote) { + spdySession.closeRemoteSide(streamID); + } else { + spdySession.closeLocalSide(streamID); + } + if ((closeSessionFuture != null) && spdySession.noActiveStreams()) { + closeSessionFuture.setSuccess(); + } + } + + private void removeStream(int streamID) { + spdySession.removeStream(streamID); + if ((closeSessionFuture != null) && spdySession.noActiveStreams()) { + closeSessionFuture.setSuccess(); + } + } + + private void sendGoAwayFrame(ChannelHandlerContext ctx, ChannelStateEvent e) { + // Avoid NotYetConnectedException + if (!e.getChannel().isConnected()) { + ctx.sendDownstream(e); + return; + } + + ChannelFuture future = sendGoAwayFrame(ctx, e.getChannel(), null); + if (spdySession.noActiveStreams()) { + future.addListener(new ClosingChannelFutureListener(ctx, e)); + } else { + closeSessionFuture = Channels.future(e.getChannel()); + closeSessionFuture.addListener(new ClosingChannelFutureListener(ctx, e)); + } + } + + private synchronized ChannelFuture sendGoAwayFrame( + ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress) { + if (!sentGoAwayFrame) { + sentGoAwayFrame = true; + ChannelFuture future = Channels.future(channel); + Channels.write(ctx, future, new DefaultSpdyGoAwayFrame(lastGoodStreamID)); + 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(); + } + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java new file mode 100644 index 0000000000..dcbc71d75d --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.Set; + +/** + * A SPDY Protocol SETTINGS Control Frame + */ +public interface SpdySettingsFrame { + + int SETTINGS_UPLOAD_BANDWIDTH = 1; + int SETTINGS_DOWNLOAD_BANDWIDTH = 2; + int SETTINGS_ROUND_TRIP_TIME = 3; + int SETTINGS_MAX_CONCURRENT_STREAMS = 4; + int SETTINGS_CURRENT_CWND = 5; + int SETTINGS_DOWNLOAD_RETRANS_RATE = 6; + int SETTINGS_INITIAL_WINDOW_SIZE = 7; + + /** + * Returns a {@code Set} of the setting IDs. + * The set's iterator will return the IDs in ascending order. + */ + Set getIDs(); + + /** + * Returns {@code true} if the setting ID has a value. + */ + boolean isSet(int ID); + + /** + * Returns the value of the setting ID. + * Returns -1 if the setting ID is not set. + */ + int getValue(int ID); + + /** + * Sets the value of the setting ID. + * The ID must be positive and cannot exceeed 16777215. + */ + void setValue(int ID, int value); + + /** + * Sets the value of the setting ID. + * Sets if the setting should be persisted (should only be set by the server). + * Sets if the setting is persisted (should only be set by the client). + * The ID must be positive and cannot exceed 16777215. + */ + void setValue(int ID, int value, boolean persistVal, boolean persisted); + + /** + * Removes the value of the setting ID. + * Removes all persistance information for the setting. + */ + void removeValue(int ID); + + /** + * Returns {@code true} if this setting should be persisted. + * Returns {@code false} if this setting should not be persisted + * or if the setting ID has no value. + */ + boolean persistValue(int ID); + + /** + * Sets if this setting should be persisted. + * Has no effect if the setting ID has no value. + */ + void setPersistValue(int ID, boolean persistValue); + + /** + * Returns {@code true} if this setting is persisted. + * Returns {@code false} if this setting should not be persisted + * or if the setting ID has no value. + */ + boolean isPersisted(int ID); + + /** + * Sets if this setting is persisted. + * Has no effect if the setting ID has no value. + */ + void setPersisted(int ID, boolean persisted); + + /** + * Returns {@code true} if previously persisted settings should be cleared. + */ + boolean clearPreviouslyPersistedSettings(); + + /** + * Sets if previously persisted settings should be cleared. + */ + void setClearPreviouslyPersistedSettings(boolean clear); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java new file mode 100644 index 0000000000..d570318266 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * The SPDY stream status code and its description. + * @apiviz.exclude + */ +public class SpdyStreamStatus implements Comparable { + + /** + * 1 Protocol Error + */ + public static final SpdyStreamStatus PROTOCOL_ERROR = + new SpdyStreamStatus(1, "PROTOCOL_ERROR"); + + /** + * 2 Invalid Stream + */ + public static final SpdyStreamStatus INVALID_STREAM = + new SpdyStreamStatus(2, "INVALID_STREAM"); + + /** + * 3 Refused Stream + */ + public static final SpdyStreamStatus REFUSED_STREAM = + new SpdyStreamStatus(3, "REFUSED_STREAM"); + + /** + * 4 Unsupported Version + */ + public static final SpdyStreamStatus UNSUPPORTED_VERSION = + new SpdyStreamStatus(4, "UNSUPPORTED_VERSION"); + + /** + * 5 Cancel + */ + public static final SpdyStreamStatus CANCEL = + new SpdyStreamStatus(5, "CANCEL"); + + /** + * 6 Internal Error + */ + public static final SpdyStreamStatus INTERNAL_ERROR = + new SpdyStreamStatus(6, "INTERNAL_ERROR"); + + /** + * 7 Flow Control Error + */ + public static final SpdyStreamStatus FLOW_CONTROL_ERROR = + new SpdyStreamStatus(7, "FLOW_CONTROL_ERROR"); + + /** + * Returns the {@link SpdyStreamStatus} represented by the specified code. + * If the specified code is a defined SPDY status code, a cached instance + * will be returned. Otherwise, a new instance will be returned. + */ + public static SpdyStreamStatus valueOf(int code) { + if (code == 0) { + throw new IllegalArgumentException( + "0 is not a valid status code for a RST_STREAM"); + } + + switch (code) { + case 1: + return PROTOCOL_ERROR; + case 2: + return INVALID_STREAM; + case 3: + return REFUSED_STREAM; + case 4: + return UNSUPPORTED_VERSION; + case 5: + return CANCEL; + case 6: + return INTERNAL_ERROR; + case 7: + return FLOW_CONTROL_ERROR; + } + + return new SpdyStreamStatus(code, "UNKNOWN (" + code + ')'); + } + + private final int code; + + private final String statusPhrase; + + /** + * Creates a new instance with the specified {@code code} and its + * {@code statusPhrase}. + */ + public SpdyStreamStatus(int code, String statusPhrase) { + if (code == 0) { + throw new IllegalArgumentException( + "0 is not a valid status code for a RST_STREAM"); + } + + if (statusPhrase == null) { + throw new NullPointerException("statusPhrase"); + } + + this.code = code; + this.statusPhrase = statusPhrase; + } + + /** + * Returns the code of this status. + */ + public int getCode() { + return code; + } + + /** + * Returns the status phrase of this status. + */ + public String getStatusPhrase() { + return statusPhrase; + } + + @Override + public int hashCode() { + return getCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SpdyStreamStatus)) { + return false; + } + + return getCode() == ((SpdyStreamStatus) o).getCode(); + } + + @Override + public String toString() { + return getStatusPhrase(); + } + + public int compareTo(SpdyStreamStatus o) { + return getCode() - o.getCode(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynReplyFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynReplyFrame.java new file mode 100644 index 0000000000..c91a7941d1 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynReplyFrame.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * A SPDY Protocol SYN_REPLY Control Frame + */ +public interface SpdySynReplyFrame extends SpdyHeaderBlock { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); + + /** + * Returns {@code true} if this frame is the last frame to be transmitted + * on the stream. + */ + boolean isLast(); + + /** + * Sets if this frame is the last frame to be transmitted on the stream. + */ + void setLast(boolean last); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java new file mode 100644 index 0000000000..70c9fb4d07 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +/** + * A SPDY Protocol SYN_STREAM Control Frame + */ +public interface SpdySynStreamFrame extends SpdyHeaderBlock { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); + + /** + * Returns the Associated-To-Stream-ID of this frame. + */ + int getAssociatedToStreamID(); + + /** + * Sets the Associated-To-Stream-ID of this frame. + * The Associated-To-Stream-ID cannot be negative. + */ + void setAssociatedToStreamID(int associatedToStreamID); + + /** + * Returns the priority of the stream. + */ + byte getPriority(); + + /** + * Sets the priority of the stream. + * The priority must be between 0 and 3 inclusive. + */ + void setPriority(byte priority); + + /** + * Returns {@code true} if this frame is the last frame to be transmitted + * on the stream. + */ + boolean isLast(); + + /** + * Sets if this frame is the last frame to be transmitted on the stream. + */ + void setLast(boolean last); + + /** + * Returns {@code true} if the stream created with this frame is to be + * considered half-closed to the receiver. + */ + boolean isUnidirectional(); + + /** + * Sets if the stream created with this frame is to be considered + * half-closed to the receiver. + */ + void setUnidirectional(boolean unidirectional); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/package-info.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/package-info.java new file mode 100644 index 0000000000..303bb3fb92 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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. + */ + +/** + * Encoder, decoder, session handler and their related message types for the SPDY protocol. + * + * @apiviz.exclude ^java\.lang\. + * @apiviz.exclude OneToOne(Encoder|Decoder)$ + * @apiviz.exclude \.SpdyHeaders\. + * @apiviz.exclude \.codec\.frame\. + * @apiviz.exclude \.(Simple)?Channel[A-Za-z]*Handler$ + * @apiviz.exclude \.Default + * @apiviz.exclude \.SpdyFrameCodec$ + */ +package io.netty.handler.codec.spdy; diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java new file mode 100644 index 0000000000..ae3f4f12b3 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java @@ -0,0 +1,252 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Random; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +import io.netty.bootstrap.ClientBootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; +import io.netty.channel.Channel; +import io.netty.channel.Channels; +import io.netty.channel.ChannelFactory; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelStateEvent; +import io.netty.channel.ExceptionEvent; +import io.netty.channel.MessageEvent; +import io.netty.channel.SimpleChannelUpstreamHandler; +import io.netty.util.internal.ExecutorUtil; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public abstract class AbstractSocketSpdyEchoTest { + + private static final Random random = new Random(); + static final ChannelBuffer frames = ChannelBuffers.buffer(1160); + static final int ignoredBytes = 20; + + private static ExecutorService executor; + + static { + // SPDY UNKNOWN Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(0xFFFF); + frames.writeByte(0xFF); + frames.writeMedium(4); + frames.writeInt(random.nextInt()); + + // SPDY NOOP Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(5); + frames.writeInt(0); + + // SPDY Data Frame + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeByte(0x01); + frames.writeMedium(1024); + for (int i = 0; i < 256; i ++) { + frames.writeInt(random.nextInt()); + } + + // SPDY SYN_STREAM Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(1); + frames.writeByte(0x03); + frames.writeMedium(12); + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeShort(0x8000); + frames.writeShort(0); + + // SPDY SYN_REPLY Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(2); + frames.writeByte(0x01); + frames.writeMedium(8); + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeInt(0); + + // SPDY RST_STREAM Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(3); + frames.writeInt(8); + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeInt(random.nextInt() | 0x01); + + // SPDY SETTINGS Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(4); + frames.writeByte(0x01); + frames.writeMedium(12); + frames.writeInt(1); + frames.writeMedium(random.nextInt()); + frames.writeByte(0x03); + frames.writeInt(random.nextInt()); + + // SPDY PING Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(6); + frames.writeInt(4); + frames.writeInt(random.nextInt()); + + // SPDY GOAWAY Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(7); + frames.writeInt(4); + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + + // SPDY HEADERS Frame + frames.writeByte(0x80); + frames.writeByte(2); + frames.writeShort(8); + frames.writeInt(4); + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + } + + @BeforeClass + public static void init() { + executor = Executors.newCachedThreadPool(); + } + + @AfterClass + public static void destroy() { + ExecutorUtil.terminate(executor); + } + + protected abstract ChannelFactory newServerSocketChannelFactory(Executor executor); + protected abstract ChannelFactory newClientSocketChannelFactory(Executor executor); + + @Test + public void testSpdyEcho() throws Throwable { + ServerBootstrap sb = new ServerBootstrap(newServerSocketChannelFactory(executor)); + ClientBootstrap cb = new ClientBootstrap(newClientSocketChannelFactory(executor)); + + EchoHandler sh = new EchoHandler(true); + EchoHandler ch = new EchoHandler(false); + + sb.getPipeline().addLast("decoder", new SpdyFrameDecoder()); + sb.getPipeline().addLast("encoder", new SpdyFrameEncoder()); + sb.getPipeline().addLast("handler", sh); + + cb.getPipeline().addLast("handler", ch); + + Channel sc = sb.bind(new InetSocketAddress(0)); + int port = ((InetSocketAddress) sc.getLocalAddress()).getPort(); + + ChannelFuture ccf = cb.connect(new InetSocketAddress(InetAddress.getLocalHost(), port)); + assertTrue(ccf.awaitUninterruptibly().isSuccess()); + + Channel cc = ccf.getChannel(); + cc.write(frames); + + while (ch.counter < frames.writerIndex() - ignoredBytes) { + if (sh.exception.get() != null) { + break; + } + if (ch.exception.get() != null) { + break; + } + + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // Ignore. + } + } + + sh.channel.close().awaitUninterruptibly(); + ch.channel.close().awaitUninterruptibly(); + sc.close().awaitUninterruptibly(); + + if (sh.exception.get() != null && !(sh.exception.get() instanceof IOException)) { + throw sh.exception.get(); + } + if (ch.exception.get() != null && !(ch.exception.get() instanceof IOException)) { + throw ch.exception.get(); + } + if (sh.exception.get() != null) { + throw sh.exception.get(); + } + if (ch.exception.get() != null) { + throw ch.exception.get(); + } + } + + private class EchoHandler extends SimpleChannelUpstreamHandler { + volatile Channel channel; + final AtomicReference exception = new AtomicReference(); + volatile int counter; + final boolean server; + + EchoHandler(boolean server) { + super(); + this.server = server; + } + + @Override + public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + channel = e.getChannel(); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + if (server) { + Channels.write(channel, e.getMessage(), e.getRemoteAddress()); + } else { + ChannelBuffer m = (ChannelBuffer) e.getMessage(); + byte[] actual = new byte[m.readableBytes()]; + m.getBytes(0, actual); + + int lastIdx = counter; + for (int i = 0; i < actual.length; i ++) { + assertEquals(frames.getByte(ignoredBytes + i + lastIdx), actual[i]); + } + + counter += actual.length; + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) + throws Exception { + if (exception.compareAndSet(null, e.getCause())) { + e.getChannel().close(); + } + } + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/NioNioSocketSpdyEchoTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/NioNioSocketSpdyEchoTest.java new file mode 100644 index 0000000000..d496b871e3 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/NioNioSocketSpdyEchoTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.concurrent.Executor; + +import io.netty.channel.ChannelFactory; +import io.netty.channel.socket.nio.NioClientSocketChannelFactory; +import io.netty.channel.socket.nio.NioServerSocketChannelFactory; + +public class NioNioSocketSpdyEchoTest extends AbstractSocketSpdyEchoTest { + + @Override + protected ChannelFactory newClientSocketChannelFactory(Executor executor) { + return new NioClientSocketChannelFactory(executor, executor); + } + + @Override + protected ChannelFactory newServerSocketChannelFactory(Executor executor) { + return new NioServerSocketChannelFactory(executor, executor); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/NioOioSocketSpdyEchoTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/NioOioSocketSpdyEchoTest.java new file mode 100644 index 0000000000..6f7e2a5368 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/NioOioSocketSpdyEchoTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.concurrent.Executor; + +import io.netty.channel.ChannelFactory; +import io.netty.channel.socket.nio.NioClientSocketChannelFactory; +import io.netty.channel.socket.oio.OioServerSocketChannelFactory; + +public class NioOioSocketSpdyEchoTest extends AbstractSocketSpdyEchoTest { + + @Override + protected ChannelFactory newClientSocketChannelFactory(Executor executor) { + return new NioClientSocketChannelFactory(executor, executor); + } + + @Override + protected ChannelFactory newServerSocketChannelFactory(Executor executor) { + return new OioServerSocketChannelFactory(executor, executor); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/OioNioSocketSpdyEchoTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/OioNioSocketSpdyEchoTest.java new file mode 100644 index 0000000000..20b55d15e2 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/OioNioSocketSpdyEchoTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.concurrent.Executor; + +import io.netty.channel.ChannelFactory; +import io.netty.channel.socket.nio.NioServerSocketChannelFactory; +import io.netty.channel.socket.oio.OioClientSocketChannelFactory; + +public class OioNioSocketSpdyEchoTest extends AbstractSocketSpdyEchoTest { + + @Override + protected ChannelFactory newClientSocketChannelFactory(Executor executor) { + return new OioClientSocketChannelFactory(executor); + } + + @Override + protected ChannelFactory newServerSocketChannelFactory(Executor executor) { + return new NioServerSocketChannelFactory(executor, executor); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/OioOioSocketSpdyEchoTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/OioOioSocketSpdyEchoTest.java new file mode 100644 index 0000000000..2cd428b38a --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/OioOioSocketSpdyEchoTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.concurrent.Executor; + +import io.netty.channel.ChannelFactory; +import io.netty.channel.socket.oio.OioClientSocketChannelFactory; +import io.netty.channel.socket.oio.OioServerSocketChannelFactory; + +public class OioOioSocketSpdyEchoTest extends AbstractSocketSpdyEchoTest { + + @Override + protected ChannelFactory newClientSocketChannelFactory(Executor executor) { + return new OioClientSocketChannelFactory(executor); + } + + @Override + protected ChannelFactory newServerSocketChannelFactory(Executor executor) { + return new OioServerSocketChannelFactory(executor, executor); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java new file mode 100644 index 0000000000..6c45718ded --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java @@ -0,0 +1,334 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * 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.netty.handler.codec.spdy; + +import java.util.List; +import java.util.Map; + +import io.netty.channel.Channels; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelStateEvent; +import io.netty.channel.MessageEvent; +import io.netty.channel.SimpleChannelUpstreamHandler; +import io.netty.handler.codec.embedder.DecoderEmbedder; +import org.junit.Assert; +import org.junit.Test; + +public class SpdySessionHandlerTest { + + private static final int closeSignal = SpdyCodecUtil.SPDY_SETTINGS_MAX_ID; + private static final SpdySettingsFrame closeMessage = new DefaultSpdySettingsFrame(); + + static { + closeMessage.setValue(closeSignal, 0); + } + + private void assertHeaderBlock(SpdyHeaderBlock received, SpdyHeaderBlock expected) { + for (String name: expected.getHeaderNames()) { + List expectedValues = expected.getHeaders(name); + List receivedValues = received.getHeaders(name); + Assert.assertTrue(receivedValues.containsAll(expectedValues)); + receivedValues.removeAll(expectedValues); + Assert.assertTrue(receivedValues.isEmpty()); + received.removeHeader(name); + } + Assert.assertTrue(received.getHeaders().isEmpty()); + } + + private void assertDataFrame(Object msg, int streamID, boolean last) { + Assert.assertNotNull(msg); + Assert.assertTrue(msg instanceof SpdyDataFrame); + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + Assert.assertTrue(spdyDataFrame.getStreamID() == streamID); + Assert.assertTrue(spdyDataFrame.isLast() == last); + } + + private void assertSynReply(Object msg, int streamID, boolean last, SpdyHeaderBlock headers) { + Assert.assertNotNull(msg); + Assert.assertTrue(msg instanceof SpdySynReplyFrame); + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + Assert.assertTrue(spdySynReplyFrame.getStreamID() == streamID); + Assert.assertTrue(spdySynReplyFrame.isLast() == last); + assertHeaderBlock(spdySynReplyFrame, headers); + } + + private void assertRstStream(Object msg, int streamID, SpdyStreamStatus status) { + Assert.assertNotNull(msg); + Assert.assertTrue(msg instanceof SpdyRstStreamFrame); + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + Assert.assertTrue(spdyRstStreamFrame.getStreamID() == streamID); + Assert.assertTrue(spdyRstStreamFrame.getStatus().equals(status)); + } + + private void assertPing(Object msg, int ID) { + Assert.assertNotNull(msg); + Assert.assertTrue(msg instanceof SpdyPingFrame); + SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; + Assert.assertTrue(spdyPingFrame.getID() == ID); + } + + private void assertGoAway(Object msg, int lastGoodStreamID) { + Assert.assertNotNull(msg); + Assert.assertTrue(msg instanceof SpdyGoAwayFrame); + SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; + Assert.assertTrue(spdyGoAwayFrame.getLastGoodStreamID() == lastGoodStreamID); + } + + private void assertHeaders(Object msg, int streamID, SpdyHeaderBlock headers) { + Assert.assertNotNull(msg); + Assert.assertTrue(msg instanceof SpdyHeadersFrame); + SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; + Assert.assertTrue(spdyHeadersFrame.getStreamID() == streamID); + assertHeaderBlock(spdyHeadersFrame, headers); + } + + private void testSpdySessionHandler(boolean server) { + DecoderEmbedder sessionHandler = + new DecoderEmbedder( + new SpdySessionHandler(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); + Assert.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); + Assert.assertNull(sessionHandler.peek()); + remoteStreamID += 2; + + // 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)); + Assert.assertNull(sessionHandler.peek()); + sessionHandler.offer(new DefaultSpdySynReplyFrame(remoteStreamID)); + assertRstStream(sessionHandler.poll(), remoteStreamID, SpdyStreamStatus.PROTOCOL_ERROR); + Assert.assertNull(sessionHandler.peek()); + remoteStreamID += 2; + + // Check if frame codec correctly compresses/uncompresses headers + sessionHandler.offer(spdySynStreamFrame); + assertSynReply(sessionHandler.poll(), localStreamID, false, spdySynStreamFrame); + Assert.assertNull(sessionHandler.peek()); + SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(localStreamID); + spdyHeadersFrame.addHeader("HEADER","test1"); + spdyHeadersFrame.addHeader("HEADER","test2"); + sessionHandler.offer(spdyHeadersFrame); + assertHeaders(sessionHandler.poll(), localStreamID, spdyHeadersFrame); + Assert.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); + Assert.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)); + Assert.assertNull(sessionHandler.peek()); + remoteStreamID += 2; + + // Check if session handler honors UNIDIRECTIONAL streams + spdySynStreamFrame.setLast(false); + sessionHandler.offer(spdySynStreamFrame); + Assert.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); + Assert.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); + Assert.assertNull(sessionHandler.peek()); + spdySynStreamFrame.setStreamID(localStreamID); + + // 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, 2); + sessionHandler.offer(spdySettingsFrame); + Assert.assertNull(sessionHandler.peek()); + sessionHandler.offer(spdySynStreamFrame); + assertRstStream(sessionHandler.poll(), localStreamID, SpdyStreamStatus.REFUSED_STREAM); + Assert.assertNull(sessionHandler.peek()); + spdySettingsFrame.setValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS, 4); + sessionHandler.offer(spdySettingsFrame); + Assert.assertNull(sessionHandler.peek()); + sessionHandler.offer(spdySynStreamFrame); + assertSynReply(sessionHandler.poll(), localStreamID, false, spdySynStreamFrame); + Assert.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()); + Assert.assertNull(sessionHandler.peek()); + spdyHeadersFrame.setStreamID(testStreamID); + sessionHandler.offer(spdyHeadersFrame); + assertRstStream(sessionHandler.poll(), testStreamID, SpdyStreamStatus.INVALID_STREAM); + Assert.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); + Assert.assertNull(sessionHandler.peek()); + + // Check if session handler returns identical local PINGs + sessionHandler.offer(localPingFrame); + assertPing(sessionHandler.poll(), localPingFrame.getID()); + Assert.assertNull(sessionHandler.peek()); + + // Check if session handler ignores un-initiated remote PINGs + sessionHandler.offer(remotePingFrame); + Assert.assertNull(sessionHandler.peek()); + + // Check if session handler sends a GOAWAY frame when closing + sessionHandler.offer(closeMessage); + assertGoAway(sessionHandler.poll(), localStreamID); + Assert.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); + Assert.assertNull(sessionHandler.peek()); + + // Check if session handler ignores Data frames after sending + // a GOAWAY frame + spdyDataFrame.setStreamID(localStreamID); + sessionHandler.offer(spdyDataFrame); + Assert.assertNull(sessionHandler.peek()); + + sessionHandler.finish(); + } + + @Test + public void testSpdyClientSessionHandler() { + testSpdySessionHandler(false); + } + + @Test + public void testSpdyServerSessionHandler() { + testSpdySessionHandler(true); + } + + // Echo Handler opens 4 half-closed streams on session connection + // and then sets the number of concurrent streams to 3 + private class EchoHandler extends SimpleChannelUpstreamHandler { + private int closeSignal; + private boolean server; + + EchoHandler(int closeSignal, boolean server) { + super(); + 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 SpdyDataFrame) || + (msg instanceof SpdyPingFrame) || + (msg instanceof SpdyHeadersFrame)) { + + Channels.write(e.getChannel(), msg, e.getRemoteAddress()); + return; + } + + if (msg instanceof SpdySynStreamFrame) { + + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + + int streamID = spdySynStreamFrame.getStreamID(); + SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID); + spdySynReplyFrame.setLast(spdySynStreamFrame.isLast()); + for (Map.Entry entry: spdySynStreamFrame.getHeaders()) { + spdySynReplyFrame.addHeader(entry.getKey(), entry.getValue()); + } + + Channels.write(e.getChannel(), spdySynReplyFrame, e.getRemoteAddress()); + return; + } + + if (msg instanceof SpdySettingsFrame) { + + SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; + if (spdySettingsFrame.isSet(closeSignal)) { + Channels.close(e.getChannel()); + } + } + } + } +}