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..7eac3b2646 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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); + } + + @Override + public int getStreamID() { + return streamID; + } + + @Override + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + @Override + public boolean isLast() { + return last; + } + + @Override + public void setLast(boolean last) { + this.last = last; + } + + @Override + public boolean isCompressed() { + return compressed; + } + + @Override + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + + @Override + public ChannelBuffer getData() { + return data; + } + + @Override + 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..4f7217c94d --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyGoAwayFrame} implementation. + */ +public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame { + + private int lastGoodStreamID; + private SpdySessionStatus status; + + /** + * Creates a new instance. + * + * @param lastGoodStreamID the Last-good-stream-ID of this frame + */ + public DefaultSpdyGoAwayFrame(int lastGoodStreamID) { + this(lastGoodStreamID, 0); + } + + /** + * Creates a new instance. + * + * @param lastGoodStreamID the Last-good-stream-ID of this frame + * @param statusCode the Status code of this frame + */ + public DefaultSpdyGoAwayFrame(int lastGoodStreamID, int statusCode) { + this(lastGoodStreamID, SpdySessionStatus.valueOf(statusCode)); + } + + /** + * Creates a new instance. + * + * @param lastGoodStreamID the Last-good-stream-ID of this frame + * @param status the status of this frame + */ + public DefaultSpdyGoAwayFrame(int lastGoodStreamID, SpdySessionStatus status) { + setLastGoodStreamID(lastGoodStreamID); + setStatus(status); + } + + @Override + public int getLastGoodStreamID() { + return lastGoodStreamID; + } + + @Override + public void setLastGoodStreamID(int lastGoodStreamID) { + if (lastGoodStreamID < 0) { + throw new IllegalArgumentException("Last-good-stream-ID" + + " cannot be negative: " + lastGoodStreamID); + } + this.lastGoodStreamID = lastGoodStreamID; + } + + @Override + public SpdySessionStatus getStatus() { + return status; + } + + @Override + public void setStatus(SpdySessionStatus status) { + this.status = status; + } + + @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); + 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/DefaultSpdyHeaderBlock.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java new file mode 100644 index 0000000000..55101edc4d --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 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() { + } + + @Override + public boolean isInvalid() { + return invalid; + } + + @Override + public void setInvalid() { + invalid = true; + } + + @Override + public void addHeader(final String name, final Object value) { + headers.addHeader(name, value); + } + + @Override + public void setHeader(final String name, final Object value) { + headers.setHeader(name, value); + } + + @Override + public void setHeader(final String name, final Iterable values) { + headers.setHeader(name, values); + } + + @Override + public void removeHeader(final String name) { + headers.removeHeader(name); + } + + @Override + public void clearHeaders() { + headers.clearHeaders(); + } + + @Override + public String getHeader(final String name) { + return headers.getHeader(name); + } + + @Override + public List getHeaders(final String name) { + return headers.getHeaders(name); + } + + @Override + public List> getHeaders() { + return headers.getHeaders(); + } + + @Override + public boolean containsHeader(final String name) { + return headers.containsHeader(name); + } + + @Override + 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..3bb95c42b8 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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; + private boolean last; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + */ + public DefaultSpdyHeadersFrame(int streamID) { + super(); + setStreamID(streamID); + } + + @Override + public int getStreamID() { + return streamID; + } + + @Override + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + @Override + public boolean isLast() { + return last; + } + + @Override + 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/DefaultSpdyNoOpFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java new file mode 100644 index 0000000000..e3c3e02d3a --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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..3ef5fa40bb --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyPingFrame.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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); + } + + @Override + public int getID() { + return ID; + } + + @Override + 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..66940af8b2 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyRstStreamFrame.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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); + } + + @Override + public int getStreamID() { + return streamID; + } + + @Override + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + @Override + public SpdyStreamStatus getStatus() { + return status; + } + + @Override + 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..d4f337aacd --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySettingsFrame.java @@ -0,0 +1,200 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * 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() { + } + + @Override + public Set getIDs() { + return settingsMap.keySet(); + } + + @Override + public boolean isSet(int ID) { + Integer key = new Integer(ID); + return settingsMap.containsKey(key); + } + + @Override + public int getValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getValue(); + } else { + return -1; + } + } + + @Override + public void setValue(int ID, int value) { + setValue(ID, value, false, false); + } + + @Override + 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)); + } + } + + @Override + public void removeValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.remove(key); + } + } + + @Override + public boolean persistValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getPersist(); + } else { + return false; + } + } + + @Override + public void setPersistValue(int ID, boolean persistValue) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.get(key).setPersist(persistValue); + } + } + + @Override + public boolean isPersisted(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getPersisted(); + } else { + return false; + } + } + + @Override + public void setPersisted(int ID, boolean persisted) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.get(key).setPersisted(persisted); + } + } + + @Override + public boolean clearPreviouslyPersistedSettings() { + return clear; + } + + @Override + 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..0852a45344 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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); + } + + @Override + public int getStreamID() { + return streamID; + } + + @Override + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + @Override + public boolean isLast() { + return last; + } + + @Override + 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..afab2ed3aa --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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); + } + + @Override + public int getStreamID() { + return streamID; + } + + @Override + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + @Override + public int getAssociatedToStreamID() { + return associatedToStreamID; + } + + @Override + public void setAssociatedToStreamID(int associatedToStreamID) { + if (associatedToStreamID < 0) { + throw new IllegalArgumentException( + "Associated-To-Stream-ID cannot be negative: " + + associatedToStreamID); + } + this.associatedToStreamID = associatedToStreamID; + } + + @Override + public byte getPriority() { + return priority; + } + + @Override + public void setPriority(byte priority) { + if (priority < 0 || priority > 7) { + throw new IllegalArgumentException( + "Priority must be between 0 and 7 inclusive: " + priority); + } + this.priority = priority; + } + + @Override + public boolean isLast() { + return last; + } + + @Override + public void setLast(boolean last) { + this.last = last; + } + + @Override + public boolean isUnidirectional() { + return unidirectional; + } + + @Override + 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/DefaultSpdyWindowUpdateFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java new file mode 100644 index 0000000000..113cf80574 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyWindowUpdateFrame} implementation. + */ +public class DefaultSpdyWindowUpdateFrame implements SpdyWindowUpdateFrame { + + private int streamID; + private int deltaWindowSize; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + * @param deltaWindowSize the Delta-Window-Size of this frame + */ + public DefaultSpdyWindowUpdateFrame(int streamID, int deltaWindowSize) { + setStreamID(streamID); + setDeltaWindowSize(deltaWindowSize); + } + + @Override + public int getStreamID() { + return streamID; + } + + @Override + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + @Override + public int getDeltaWindowSize() { + return deltaWindowSize; + } + + @Override + public void setDeltaWindowSize(int deltaWindowSize) { + if (deltaWindowSize <= 0) { + throw new IllegalArgumentException( + "Delta-Window-Size must be positive: " + + deltaWindowSize); + } + this.deltaWindowSize = deltaWindowSize; + } + + @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("--> Delta-Window-Size = "); + buf.append(deltaWindowSize); + 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..7f323eed37 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java @@ -0,0 +1,366 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.buffer.ChannelBuffer; + +final class SpdyCodecUtil { + + static final int SPDY_MIN_VERSION = 2; + static final int SPDY_MAX_VERSION = 3; + + 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 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 int SPDY_CREDENTIAL_FRAME = 10; + + 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 + static final byte[] SPDY_DICT = { + 0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // - - - - o p t i + 0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // o n s - - - - h + 0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // e a d - - - - p + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // o s t - - - - p + 0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // u t - - - - d e + 0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // l e t e - - - - + 0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // t r a c e - - - + 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // - a c c e p t - + 0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p + 0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t - c h a r s e + 0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t - - - - a c c + 0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e p t - e n c o + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // d i n g - - - - + 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // a c c e p t - l + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // a n g u a g e - + 0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p + 0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t - r a n g e s + 0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // - - - - a g e - + 0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // - - - a l l o w + 0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // - - - - a u t h + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // o r i z a t i o + 0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n - - - - c a c + 0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // h e - c o n t r + 0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // o l - - - - c o + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // n n e c t i o n + 0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // e n t - b a s e + 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e n t - e n c o + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // d i n g - - - - + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // c o n t e n t - + 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // l a n g u a g e + 0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // e n t - l e n g + 0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // t h - - - - c o + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // n t e n t - l o + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // c a t i o n - - + 0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n + 0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t - m d 5 - - - + 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // - c o n t e n t + 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // - r a n g e - - + 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n + 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t - t y p e - - + 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // - - d a t e - - + 0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // - - e t a g - - + 0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // - - e x p e c t + 0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // - - - - e x p i + 0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // r e s - - - - f + 0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // r o m - - - - h + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // o s t - - - - i + 0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f - m a t c h - + 0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // - - - i f - m o + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // d i f i e d - s + 0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // i n c e - - - - + 0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // i f - n o n e - + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // m a t c h - - - + 0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // - i f - r a n g + 0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e - - - - i f - + 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // u n m o d i f i + 0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // e d - s i n c e + 0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // - - - - l a s t + 0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // - m o d i f i e + 0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d - - - - l o c + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // a t i o n - - - + 0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // - m a x - f o r + 0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // w a r d s - - - + 0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // - p r a g m a - + 0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // - - - p r o x y + 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // - a u t h e n t + 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // i c a t e - - - + 0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // - p r o x y - a + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // u t h o r i z a + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // t i o n - - - - + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // r a n g e - - - + 0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // - r e f e r e r + 0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // - - - - r e t r + 0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y - a f t e r - + 0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // - - - s e r v e + 0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r - - - - t e - + 0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // - - - t r a i l + 0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // e r - - - - t r + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // a n s f e r - e + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // n c o d i n g - + 0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // - - - u p g r a + 0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // d e - - - - u s + 0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // e r - a g e n t + 0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // - - - - v a r y + 0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // - - - - v i a - + 0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // - - - w a r n i + 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // n g - - - - w w + 0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w - a u t h e n + 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // t i c a t e - - + 0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // - - m e t h o d + 0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // - - - - g e t - + 0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // - - - s t a t u + 0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s - - - - 2 0 0 + 0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // - O K - - - - v + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // e r s i o n - - + 0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // - - H T T P - 1 + 0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // - 1 - - - - u r + 0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l - - - - p u b + 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // l i c - - - - s + 0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // e t - c o o k i + 0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e - - - - k e e + 0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p - a l i v e - + 0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // - - - o r i g i + 0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n 1 0 0 1 0 1 2 + 0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 0 1 2 0 2 2 0 5 + 0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 2 0 6 3 0 0 3 0 + 0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 2 3 0 3 3 0 4 3 + 0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 0 5 3 0 6 3 0 7 + 0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 4 0 2 4 0 5 4 0 + 0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 6 4 0 7 4 0 8 4 + 0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 0 9 4 1 0 4 1 1 + 0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 4 1 2 4 1 3 4 1 + 0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 4 4 1 5 4 1 6 4 + 0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 1 7 5 0 2 5 0 4 + 0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 5 0 5 2 0 3 - N + 0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // o n - A u t h o + 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // r i t a t i v e + 0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // - I n f o r m a + 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // t i o n 2 0 4 - + 0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // N o - C o n t e + 0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // n t 3 0 1 - M o + 0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // v e d - P e r m + 0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // a n e n t l y 4 + 0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 0 0 - B a d - R + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // e q u e s t 4 0 + 0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1 - U n a u t h + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // o r i z e d 4 0 + 0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3 - F o r b i d + 0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // d e n 4 0 4 - N + 0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // o t - F o u n d + 0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 5 0 0 - I n t e + 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // r n a l - S e r + 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // v e r - E r r o + 0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r 5 0 1 - N o t + 0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // - I m p l e m e + 0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // n t e d 5 0 3 - + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // S e r v i c e - + 0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // U n a v a i l a + 0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // b l e J a n - F + 0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // e b - M a r - A + 0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // p r - M a y - J + 0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // u n - J u l - A + 0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // u g - S e p t - + 0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // O c t - N o v - + 0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // D e c - 0 0 - 0 + 0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0 - 0 0 - M o n + 0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // - - T u e - - W + 0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // e d - - T h u - + 0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // - F r i - - S a + 0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t - - S u n - - + 0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // G M T c h u n k + 0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // e d - t e x t - + 0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // h t m l - i m a + 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // g e - p n g - i + 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // m a g e - j p g + 0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // - i m a g e - g + 0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // i f - a p p l i + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x + 0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // m l - a p p l i + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x + 0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // h t m l - x m l + 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // - t e x t - p l + 0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // a i n - t e x t + 0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // - j a v a s c r + 0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // i p t - p u b l + 0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // i c p r i v a t + 0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // e m a x - a g e + 0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // - g z i p - d e + 0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // f l a t e - s d + 0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // c h c h a r s e + 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t - u t f - 8 c + 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // h a r s e t - i + 0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // s o - 8 8 5 9 - + 0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1 - u t f - - - + 0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e // - e n q - 0 - + }; + + private static final String SPDY2_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[] SPDY2_DICT; + static { + byte[] SPDY2_DICT_ = null; + + try { + SPDY2_DICT_ = SPDY2_DICT_S.getBytes("US-ASCII"); + // dictionary is null terminated + SPDY2_DICT_[SPDY2_DICT_.length - 1] = (byte) 0; + } catch (Exception e) { + SPDY2_DICT_ = new byte[1]; + } + + SPDY2_DICT = SPDY2_DICT_; + } + + + private SpdyCodecUtil() { + } + + + /** + * Reads a big-endian unsigned short integer from the buffer. + */ + static int getUnsignedShort(ChannelBuffer buf, int offset) { + return (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 (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 (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 (buf.getByte(offset) & 0xFF) << 24 | + (buf.getByte(offset + 1) & 0xFF) << 16 | + (buf.getByte(offset + 2) & 0xFF) << 8 | + buf.getByte(offset + 3) & 0xFF; + } + + /** + * Returns {@code true} if ID is for a server initiated stream or ping. + */ + static boolean isServerID(int ID) { + // Server initiated streams and pings have even IDs + return ID % 2 == 0; + } + + /** + * 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..19f072b906 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyDataFrame.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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); + + /** + * @deprecated Removed from SPDY specification. + */ + @Deprecated + boolean isCompressed(); + + /** + * @deprecated Removed from SPDY specification. + */ + @Deprecated + 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..b417e8f866 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.channel.CombinedChannelHandler; + +/** + * 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 extends CombinedChannelHandler { + + /** + * Creates a new instance with the specified {@code version} and + * the default decoder and encoder options + * ({@code maxChunkSize (8192)}, {@code maxHeaderSize (16384)}, + * {@code compressionLevel (6)}, {@code windowBits (15)}, + * and {@code memLevel (8)}). + */ + public SpdyFrameCodec(int version) { + this(version, 8192, 16384, 6, 15, 8); + } + + /** + * Creates a new instance with the specified decoder and encoder options. + */ + public SpdyFrameCodec( + int version, int maxChunkSize, int maxHeaderSize, + int compressionLevel, int windowBits, int memLevel) { + super( + new SpdyFrameDecoder(version, maxChunkSize, maxHeaderSize), + new SpdyFrameEncoder(version, compressionLevel, windowBits, memLevel)); + } +} 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..a8a085bdae --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java @@ -0,0 +1,790 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerContext; +import io.netty.handler.codec.StreamToMessageDecoder; +import io.netty.handler.codec.TooLongFrameException; + +/** + * Decodes {@link ChannelBuffer}s into SPDY Data and Control Frames. + */ +public class SpdyFrameDecoder extends StreamToMessageDecoder { + + private final int spdyVersion; + private final int maxChunkSize; + private final int maxHeaderSize; + + private final SpdyHeaderBlockDecompressor headerBlockDecompressor; + + private State state; + private SpdySettingsFrame spdySettingsFrame; + private SpdyHeaderBlock spdyHeaderBlock; + + // SPDY common header fields + private byte flags; + private int length; + private int version; + private int type; + private int streamID; + + // Header block decoding fields + private int headerSize; + private int numHeaders; + private ChannelBuffer decompressed; + + private static enum State { + READ_COMMON_HEADER, + READ_CONTROL_FRAME, + READ_SETTINGS_FRAME, + READ_HEADER_BLOCK_FRAME, + READ_HEADER_BLOCK, + READ_DATA_FRAME, + DISCARD_FRAME, + FRAME_ERROR + } + + /** + * Creates a new instance with the default {@code version (2)}, + * {@code maxChunkSize (8192)}, and {@code maxHeaderSize (16384)}. + */ + @Deprecated + public SpdyFrameDecoder() { + this(2); + } + + /** + * Creates a new instance with the specified {@code version} and the default + * {@code maxChunkSize (8192)} and {@code maxHeaderSize (16384)}. + */ + public SpdyFrameDecoder(int version) { + this(version, 8192, 16384); + } + + /** + * Creates a new instance with the specified parameters. + */ + public SpdyFrameDecoder(int version, int maxChunkSize, int maxHeaderSize) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } + if (maxChunkSize <= 0) { + throw new IllegalArgumentException( + "maxChunkSize must be a positive integer: " + maxChunkSize); + } + if (maxHeaderSize <= 0) { + throw new IllegalArgumentException( + "maxHeaderSize must be a positive integer: " + maxHeaderSize); + } + spdyVersion = version; + this.maxChunkSize = maxChunkSize; + this.maxHeaderSize = maxHeaderSize; + headerBlockDecompressor = SpdyHeaderBlockDecompressor.newInstance(version); + state = State.READ_COMMON_HEADER; + } + + @Override + public Object decodeLast(ChannelInboundHandlerContext ctx, ChannelBuffer in) throws Exception { + try { + return decode(ctx, in); + } finally { + headerBlockDecompressor.end(); + } + } + + @Override + public Object decode(ChannelInboundHandlerContext ctx, ChannelBuffer buffer) throws Exception { + switch(state) { + case READ_COMMON_HEADER: + state = readCommonHeader(buffer); + if (state == State.FRAME_ERROR) { + if (version != spdyVersion) { + fireProtocolException(ctx, "Unsupported version: " + version); + } else { + fireInvalidControlFrameException(ctx); + } + } + + // FrameDecoders must consume data when producing frames + // All length 0 frames must be generated now + if (length == 0) { + if (state == State.READ_DATA_FRAME) { + if (streamID == 0) { + state = State.FRAME_ERROR; + fireProtocolException(ctx, "Received invalid data frame"); + return null; + } + + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID); + spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); + state = State.READ_COMMON_HEADER; + return spdyDataFrame; + } + // There are no length 0 control frames + state = State.READ_COMMON_HEADER; + } + + return null; + + case READ_CONTROL_FRAME: + try { + Object frame = readControlFrame(buffer); + if (frame != null) { + state = State.READ_COMMON_HEADER; + } + return frame; + } catch (IllegalArgumentException e) { + state = State.FRAME_ERROR; + fireInvalidControlFrameException(ctx); + } + return null; + + case READ_SETTINGS_FRAME: + if (spdySettingsFrame == null) { + // Validate frame length against number of entries + if (buffer.readableBytes() < 4) { + return null; + } + int numEntries = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + length -= 4; + + // Each ID/Value entry is 8 bytes + if ((length & 0x07) != 0 || length >> 3 != numEntries) { + state = State.FRAME_ERROR; + fireInvalidControlFrameException(ctx); + return null; + } + + spdySettingsFrame = new DefaultSpdySettingsFrame(); + + boolean clear = (flags & SPDY_SETTINGS_CLEAR) != 0; + spdySettingsFrame.setClearPreviouslyPersistedSettings(clear); + } + + int readableEntries = Math.min(buffer.readableBytes() >> 3, length >> 3); + for (int i = 0; i < readableEntries; i ++) { + int ID; + byte ID_flags; + if (version < 3) { + // Chromium Issue 79156 + // SPDY setting ids are not written in network byte order + // Read id assuming the architecture is little endian + ID = buffer.readByte() & 0xFF | + (buffer.readByte() & 0xFF) << 8 | + (buffer.readByte() & 0xFF) << 16; + ID_flags = buffer.readByte(); + } else { + ID_flags = buffer.readByte(); + ID = getUnsignedMedium(buffer, buffer.readerIndex()); + buffer.skipBytes(3); + } + int value = getSignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + + // Check for invalid ID -- avoid IllegalArgumentException in setValue + if (ID == 0) { + state = State.FRAME_ERROR; + spdySettingsFrame = null; + fireInvalidControlFrameException(ctx); + return null; + } + + 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); + } + } + + length -= 8 * readableEntries; + if (length == 0) { + state = State.READ_COMMON_HEADER; + Object frame = spdySettingsFrame; + spdySettingsFrame = null; + return frame; + } + return null; + + case READ_HEADER_BLOCK_FRAME: + try { + spdyHeaderBlock = readHeaderBlockFrame(buffer); + if (spdyHeaderBlock != null) { + if (length == 0) { + state = State.READ_COMMON_HEADER; + Object frame = spdyHeaderBlock; + spdyHeaderBlock = null; + return frame; + } + state = State.READ_HEADER_BLOCK; + } + return null; + } catch (IllegalArgumentException e) { + state = State.FRAME_ERROR; + fireInvalidControlFrameException(ctx); + return null; + } + + case READ_HEADER_BLOCK: + int compressedBytes = Math.min(buffer.readableBytes(), length); + length -= compressedBytes; + + try { + decodeHeaderBlock(buffer.readSlice(compressedBytes)); + } catch (Exception e) { + state = State.FRAME_ERROR; + spdyHeaderBlock = null; + decompressed = null; + ctx.fireExceptionCaught(e); + return null; + } + + if (spdyHeaderBlock != null && spdyHeaderBlock.isInvalid()) { + Object frame = spdyHeaderBlock; + spdyHeaderBlock = null; + decompressed = null; + if (length == 0) { + state = State.READ_COMMON_HEADER; + } + return frame; + } + + if (length == 0) { + Object frame = spdyHeaderBlock; + spdyHeaderBlock = null; + state = State.READ_COMMON_HEADER; + return frame; + } + return null; + + case READ_DATA_FRAME: + if (streamID == 0) { + state = State.FRAME_ERROR; + fireProtocolException(ctx, "Received invalid data frame"); + return null; + } + + // Generate data frames that do not exceed maxChunkSize + int dataLength = Math.min(maxChunkSize, length); + + // Wait until entire frame is readable + if (buffer.readableBytes() < dataLength) { + return null; + } + + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID); + spdyDataFrame.setData(buffer.readBytes(dataLength)); + length -= dataLength; + + if (length == 0) { + spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); + state = State.READ_COMMON_HEADER; + } + return spdyDataFrame; + + case DISCARD_FRAME: + int numBytes = Math.min(buffer.readableBytes(), length); + buffer.skipBytes(numBytes); + length -= numBytes; + if (length == 0) { + state = State.READ_COMMON_HEADER; + } + return null; + + case FRAME_ERROR: + buffer.skipBytes(buffer.readableBytes()); + return null; + + default: + throw new Error("Shouldn't reach here."); + } + } + + private State readCommonHeader(ChannelBuffer buffer) { + // Wait until entire header is readable + if (buffer.readableBytes() < SPDY_HEADER_SIZE) { + return State.READ_COMMON_HEADER; + } + + int frameOffset = buffer.readerIndex(); + int flagsOffset = frameOffset + SPDY_HEADER_FLAGS_OFFSET; + int lengthOffset = frameOffset + SPDY_HEADER_LENGTH_OFFSET; + buffer.skipBytes(SPDY_HEADER_SIZE); + + // Read common header fields + boolean control = (buffer.getByte(frameOffset) & 0x80) != 0; + flags = buffer.getByte(flagsOffset); + length = getUnsignedMedium(buffer, lengthOffset); + + if (control) { + // Decode control frame common header + version = getUnsignedShort(buffer, frameOffset) & 0x7FFF; + + int typeOffset = frameOffset + SPDY_HEADER_TYPE_OFFSET; + type = getUnsignedShort(buffer, typeOffset); + + // Check version first then validity + if (version != spdyVersion || !isValidControlFrameHeader()) { + return State.FRAME_ERROR; + } + + // Make sure decoder will produce a frame or consume input + State nextState; + if (willGenerateControlFrame()) { + switch (type) { + case SPDY_SYN_STREAM_FRAME: + case SPDY_SYN_REPLY_FRAME: + case SPDY_HEADERS_FRAME: + nextState = State.READ_HEADER_BLOCK_FRAME; + break; + + case SPDY_SETTINGS_FRAME: + nextState = State.READ_SETTINGS_FRAME; + break; + + default: + nextState = State.READ_CONTROL_FRAME; + } + } else if (length != 0) { + nextState = State.DISCARD_FRAME; + } else { + nextState = State.READ_COMMON_HEADER; + } + return nextState; + } else { + // Decode data frame common header + streamID = getUnsignedInt(buffer, frameOffset); + + return State.READ_DATA_FRAME; + } + } + + private Object readControlFrame(ChannelBuffer buffer) { + int streamID; + int statusCode; + switch (type) { + case SPDY_RST_STREAM_FRAME: + if (buffer.readableBytes() < 8) { + return null; + } + + streamID = getUnsignedInt(buffer, buffer.readerIndex()); + statusCode = getSignedInt(buffer, buffer.readerIndex() + 4); + buffer.skipBytes(8); + + return new DefaultSpdyRstStreamFrame(streamID, statusCode); + + case SPDY_PING_FRAME: + if (buffer.readableBytes() < 4) { + return null; + } + + int ID = getSignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + + return new DefaultSpdyPingFrame(ID); + + case SPDY_GOAWAY_FRAME: + int minLength = version < 3 ? 4 : 8; + if (buffer.readableBytes() < minLength) { + return null; + } + + int lastGoodStreamID = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + + if (version < 3) { + return new DefaultSpdyGoAwayFrame(lastGoodStreamID); + } + + statusCode = getSignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + + return new DefaultSpdyGoAwayFrame(lastGoodStreamID, statusCode); + + case SPDY_WINDOW_UPDATE_FRAME: + if (buffer.readableBytes() < 8) { + return null; + } + + streamID = getUnsignedInt(buffer, buffer.readerIndex()); + int deltaWindowSize = getUnsignedInt(buffer, buffer.readerIndex() + 4); + buffer.skipBytes(8); + + return new DefaultSpdyWindowUpdateFrame(streamID, deltaWindowSize); + + default: + throw new Error("Shouldn't reach here."); + } + } + + private SpdyHeaderBlock readHeaderBlockFrame(ChannelBuffer buffer) { + int minLength; + int streamID; + switch (type) { + case SPDY_SYN_STREAM_FRAME: + minLength = version < 3 ? 12 : 10; + if (buffer.readableBytes() < minLength) { + return null; + } + + int offset = buffer.readerIndex(); + streamID = getUnsignedInt(buffer, offset); + int associatedToStreamID = getUnsignedInt(buffer, offset + 4); + byte priority = (byte) (buffer.getByte(offset + 8) >> 5 & 0x07); + if (version < 3) { + priority >>= 1; + } + buffer.skipBytes(10); + length -= 10; + + // SPDY/2 requires 16-bits of padding for empty header blocks + if (version < 3 && length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { + buffer.skipBytes(2); + length = 0; + } + + SpdySynStreamFrame spdySynStreamFrame = + new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority); + spdySynStreamFrame.setLast((flags & SPDY_FLAG_FIN) != 0); + spdySynStreamFrame.setUnidirectional((flags & SPDY_FLAG_UNIDIRECTIONAL) != 0); + + return spdySynStreamFrame; + + case SPDY_SYN_REPLY_FRAME: + minLength = version < 3 ? 8 : 4; + if (buffer.readableBytes() < minLength) { + return null; + } + + streamID = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + length -= 4; + + // SPDY/2 has 16-bits of unused space + if (version < 3) { + buffer.skipBytes(2); + length -= 2; + } + + // SPDY/2 requires 16-bits of padding for empty header blocks + if (version < 3 && length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { + buffer.skipBytes(2); + length = 0; + } + + SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID); + spdySynReplyFrame.setLast((flags & SPDY_FLAG_FIN) != 0); + + return spdySynReplyFrame; + + case SPDY_HEADERS_FRAME: + if (buffer.readableBytes() < 4) { + return null; + } + + // SPDY/2 allows length 4 frame when there are no name/value pairs + if (version < 3 && length > 4 && buffer.readableBytes() < 8) { + return null; + } + + streamID = getUnsignedInt(buffer, buffer.readerIndex()); + buffer.skipBytes(4); + length -= 4; + + // SPDY/2 has 16-bits of unused space + if (version < 3 && length != 0) { + buffer.skipBytes(2); + length -= 2; + } + + // SPDY/2 requires 16-bits of padding for empty header blocks + if (version < 3 && length == 2 && buffer.getShort(buffer.readerIndex()) == 0) { + buffer.skipBytes(2); + length = 0; + } + + SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamID); + spdyHeadersFrame.setLast((flags & SPDY_FLAG_FIN) != 0); + + return spdyHeadersFrame; + + default: + throw new Error("Shouldn't reach here."); + } + } + + private boolean ensureBytes(int bytes) throws Exception { + if (decompressed.readableBytes() >= bytes) { + return true; + } + // Perhaps last call to decode filled output buffer + headerBlockDecompressor.decode(decompressed); + return decompressed.readableBytes() >= bytes; + } + + private int readLengthField() { + if (version < 3) { + return decompressed.readUnsignedShort(); + } else { + return decompressed.readInt(); + } + } + + private void decodeHeaderBlock(ChannelBuffer buffer) throws Exception { + if (decompressed == null) { + // First time we start to decode a header block + // Initialize header block decoding fields + headerSize = 0; + numHeaders = -1; + decompressed = ChannelBuffers.dynamicBuffer(8192); + } + + // Accumulate decompressed data + headerBlockDecompressor.setInput(buffer); + headerBlockDecompressor.decode(decompressed); + + if (spdyHeaderBlock == null) { + // Only decompressing data to keep decompression context in sync + decompressed = null; + return; + } + + int lengthFieldSize = version < 3 ? 2 : 4; + + if (numHeaders == -1) { + // Read number of Name/Value pairs + if (decompressed.readableBytes() < lengthFieldSize) { + return; + } + numHeaders = readLengthField(); + if (numHeaders < 0) { + spdyHeaderBlock.setInvalid(); + return; + } + } + + while (numHeaders > 0) { + int headerSize = this.headerSize; + decompressed.markReaderIndex(); + + // Try to read length of name + if (!ensureBytes(lengthFieldSize)) { + decompressed.resetReaderIndex(); + decompressed.discardReadBytes(); + return; + } + int nameLength = readLengthField(); + + // Recipients of a zero-length name must issue a stream error + if (nameLength <= 0) { + spdyHeaderBlock.setInvalid(); + return; + } + headerSize += nameLength; + if (headerSize > maxHeaderSize) { + throw new TooLongFrameException( + "Header block exceeds " + maxHeaderSize); + } + + // Try to read name + if (!ensureBytes(nameLength)) { + decompressed.resetReaderIndex(); + decompressed.discardReadBytes(); + return; + } + byte[] nameBytes = new byte[nameLength]; + decompressed.readBytes(nameBytes); + String name = new String(nameBytes, "UTF-8"); + + // Check for identically named headers + if (spdyHeaderBlock.containsHeader(name)) { + spdyHeaderBlock.setInvalid(); + return; + } + + // Try to read length of value + if (!ensureBytes(lengthFieldSize)) { + decompressed.resetReaderIndex(); + decompressed.discardReadBytes(); + return; + } + int valueLength = readLengthField(); + + // Recipients of illegal value fields must issue a stream error + if (valueLength <= 0) { + spdyHeaderBlock.setInvalid(); + return; + } + headerSize += valueLength; + if (headerSize > maxHeaderSize) { + throw new TooLongFrameException( + "Header block exceeds " + maxHeaderSize); + } + + // Try to read value + if (!ensureBytes(valueLength)) { + decompressed.resetReaderIndex(); + decompressed.discardReadBytes(); + return; + } + byte[] valueBytes = new byte[valueLength]; + decompressed.readBytes(valueBytes); + + // Add Name/Value pair to headers + int index = 0; + int offset = 0; + while (index < valueLength) { + while (index < valueBytes.length && valueBytes[index] != (byte) 0) { + index ++; + } + if (index < valueBytes.length && valueBytes[index + 1] == (byte) 0) { + // Received multiple, in-sequence NULL characters + // Recipients of illegal value fields must issue a stream error + spdyHeaderBlock.setInvalid(); + return; + } + String value = new String(valueBytes, offset, index - offset, "UTF-8"); + + try { + spdyHeaderBlock.addHeader(name, value); + } catch (IllegalArgumentException e) { + // Name contains NULL or non-ascii characters + spdyHeaderBlock.setInvalid(); + return; + } + index ++; + offset = index; + } + numHeaders --; + this.headerSize = headerSize; + } + decompressed = null; + } + + private boolean isValidControlFrameHeader() { + switch (type) { + case SPDY_SYN_STREAM_FRAME: + return version < 3 ? length >= 12 : length >= 10; + + case SPDY_SYN_REPLY_FRAME: + return version < 3 ? length >= 8 : length >= 4; + + case SPDY_RST_STREAM_FRAME: + return flags == 0 && length == 8; + + case SPDY_SETTINGS_FRAME: + return length >= 4; + + case SPDY_NOOP_FRAME: + return length == 0; + + case SPDY_PING_FRAME: + return length == 4; + + case SPDY_GOAWAY_FRAME: + return version < 3 ? length == 4 : length == 8; + + case SPDY_HEADERS_FRAME: + if (version < 3) { + return length == 4 || length >= 8; + } else { + return length >= 4; + } + + case SPDY_WINDOW_UPDATE_FRAME: + return length == 8; + + case SPDY_CREDENTIAL_FRAME: + default: + return true; + } + } + + private boolean willGenerateControlFrame() { + switch (type) { + case SPDY_SYN_STREAM_FRAME: + case SPDY_SYN_REPLY_FRAME: + case SPDY_RST_STREAM_FRAME: + case SPDY_SETTINGS_FRAME: + case SPDY_PING_FRAME: + case SPDY_GOAWAY_FRAME: + case SPDY_HEADERS_FRAME: + case SPDY_WINDOW_UPDATE_FRAME: + return true; + + case SPDY_NOOP_FRAME: + case SPDY_CREDENTIAL_FRAME: + default: + return false; + } + } + + private void fireInvalidControlFrameException(ChannelHandlerContext ctx) { + String message = "Received invalid control frame"; + switch (type) { + case SPDY_SYN_STREAM_FRAME: + message = "Received invalid SYN_STREAM control frame"; + break; + + case SPDY_SYN_REPLY_FRAME: + message = "Received invalid SYN_REPLY control frame"; + break; + + case SPDY_RST_STREAM_FRAME: + message = "Received invalid RST_STREAM control frame"; + break; + + case SPDY_SETTINGS_FRAME: + message = "Received invalid SETTINGS control frame"; + break; + + case SPDY_NOOP_FRAME: + message = "Received invalid NOOP control frame"; + break; + + case SPDY_PING_FRAME: + message = "Received invalid PING control frame"; + break; + + case SPDY_GOAWAY_FRAME: + message = "Received invalid GOAWAY control frame"; + break; + + case SPDY_HEADERS_FRAME: + message = "Received invalid HEADERS control frame"; + break; + + case SPDY_WINDOW_UPDATE_FRAME: + message = "Received invalid WINDOW_UPDATE control frame"; + break; + + case SPDY_CREDENTIAL_FRAME: + message = "Received invalid CREDENTIAL control frame"; + break; + } + fireProtocolException(ctx, message); + } + + private static void fireProtocolException(ChannelHandlerContext ctx, String message) { + ctx.fireExceptionCaught(new SpdyProtocolException(message)); + } +} 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..94a454766c --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java @@ -0,0 +1,356 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerContext; +import io.netty.handler.codec.MessageToStreamEncoder; +import io.netty.handler.codec.UnsupportedMessageTypeException; + +import java.nio.ByteOrder; +import java.util.Set; + +/** + * Encodes a SPDY Data or Control Frame into a {@link ChannelBuffer}. + */ +public class SpdyFrameEncoder extends MessageToStreamEncoder { + + private final int version; + private volatile boolean finished; + private final SpdyHeaderBlockCompressor headerBlockCompressor; + + /** + * Creates a new instance with the specified {@code version} and the + * default {@code compressionLevel (6)}, {@code windowBits (15)}, + * and {@code memLevel (8)}. + */ + public SpdyFrameEncoder(int version) { + this(version, 6, 15, 8); + } + + /** + * Creates a new instance with the specified parameters. + */ + public SpdyFrameEncoder(int version, int compressionLevel, int windowBits, int memLevel) { + super(); + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unknown version: " + version); + } + this.version = version; + headerBlockCompressor = SpdyHeaderBlockCompressor.newInstance( + version, compressionLevel, windowBits, memLevel); + } + + @Override + public void beforeAdd(ChannelHandlerContext ctx) throws Exception { + ctx.channel().closeFuture().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + synchronized (headerBlockCompressor) { + if (finished) { + return; + } + finished = true; + headerBlockCompressor.end(); + } + } + }); + } + + @Override + public boolean isEncodable(Object msg) throws Exception { + // FIXME: Introduce supertype + return msg instanceof SpdyDataFrame || + msg instanceof SpdySynStreamFrame || + msg instanceof SpdySynReplyFrame || + msg instanceof SpdyRstStreamFrame || + msg instanceof SpdySettingsFrame || + msg instanceof SpdyNoOpFrame || + msg instanceof SpdyPingFrame || + msg instanceof SpdyGoAwayFrame || + msg instanceof SpdyHeadersFrame || + msg instanceof SpdyWindowUpdateFrame; + } + + @Override + public void encode(ChannelOutboundHandlerContext ctx, Object msg, ChannelBuffer out) throws Exception { + if (msg instanceof SpdyDataFrame) { + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + ChannelBuffer data = spdyDataFrame.getData(); + byte flags = spdyDataFrame.isLast() ? SPDY_DATA_FLAG_FIN : 0; + out.ensureWritableBytes(SPDY_HEADER_SIZE + data.readableBytes()); + out.writeInt(spdyDataFrame.getStreamID() & 0x7FFFFFFF); + out.writeByte(flags); + out.writeMedium(data.readableBytes()); + out.writeBytes(data, data.readerIndex(), data.readableBytes()); + + } else if (msg instanceof SpdySynStreamFrame) { + + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(version, spdySynStreamFrame)); + byte flags = spdySynStreamFrame.isLast() ? SPDY_FLAG_FIN : 0; + if (spdySynStreamFrame.isUnidirectional()) { + flags |= SPDY_FLAG_UNIDIRECTIONAL; + } + int headerBlockLength = data.readableBytes(); + int length; + if (version < 3) { + length = headerBlockLength == 0 ? 12 : 10 + headerBlockLength; + } else { + length = 10 + headerBlockLength; + } + out.ensureWritableBytes(SPDY_HEADER_SIZE + length); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_SYN_STREAM_FRAME); + out.writeByte(flags); + out.writeMedium(length); + out.writeInt(spdySynStreamFrame.getStreamID()); + out.writeInt(spdySynStreamFrame.getAssociatedToStreamID()); + if (version < 3) { + // Restrict priorities for SPDY/2 to between 0 and 3 + byte priority = spdySynStreamFrame.getPriority(); + if (priority > 3) { + priority = 3; + } + out.writeShort((priority & 0xFF) << 14); + } else { + out.writeShort((spdySynStreamFrame.getPriority() & 0xFF) << 13); + } + if (version < 3 && data.readableBytes() == 0) { + out.writeShort(0); + } + out.writeBytes(data, data.readerIndex(), headerBlockLength); + + } else if (msg instanceof SpdySynReplyFrame) { + + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(version, spdySynReplyFrame)); + byte flags = spdySynReplyFrame.isLast() ? SPDY_FLAG_FIN : 0; + int headerBlockLength = data.readableBytes(); + int length; + if (version < 3) { + length = headerBlockLength == 0 ? 8 : 6 + headerBlockLength; + } else { + length = 4 + headerBlockLength; + } + out.ensureWritableBytes(SPDY_HEADER_SIZE + length); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_SYN_REPLY_FRAME); + out.writeByte(flags); + out.writeMedium(length); + out.writeInt(spdySynReplyFrame.getStreamID()); + if (version < 3) { + if (headerBlockLength == 0) { + out.writeInt(0); + } else { + out.writeShort(0); + } + } + out.writeBytes(data, data.readerIndex(), headerBlockLength); + + } else if (msg instanceof SpdyRstStreamFrame) { + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + out.ensureWritableBytes(SPDY_HEADER_SIZE + 8); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_RST_STREAM_FRAME); + out.writeInt(8); + out.writeInt(spdyRstStreamFrame.getStreamID()); + out.writeInt(spdyRstStreamFrame.getStatus().getCode()); + + } 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; + out.ensureWritableBytes(SPDY_HEADER_SIZE + length); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_SETTINGS_FRAME); + out.writeByte(flags); + out.writeMedium(length); + out.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; + } + if (version < 3) { + // Chromium Issue 79156 + // SPDY setting ids are not written in network byte order + // Write id assuming the architecture is little endian + out.writeByte(id >> 0 & 0xFF); + out.writeByte(id >> 8 & 0xFF); + out.writeByte(id >> 16 & 0xFF); + out.writeByte(ID_flags); + } else { + out.writeByte(ID_flags); + out.writeMedium(id); + } + out.writeInt(spdySettingsFrame.getValue(id)); + } + + } else if (msg instanceof SpdyNoOpFrame) { + + out.ensureWritableBytes(SPDY_HEADER_SIZE); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_NOOP_FRAME); + out.writeInt(0); + + } else if (msg instanceof SpdyPingFrame) { + + SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; + out.ensureWritableBytes(SPDY_HEADER_SIZE + 4); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_PING_FRAME); + out.writeInt(4); + out.writeInt(spdyPingFrame.getID()); + + } else if (msg instanceof SpdyGoAwayFrame) { + + SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; + int length = version < 3 ? 4 : 8; + out.ensureWritableBytes(SPDY_HEADER_SIZE + length); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_GOAWAY_FRAME); + out.writeInt(length); + out.writeInt(spdyGoAwayFrame.getLastGoodStreamID()); + if (version >= 3) { + out.writeInt(spdyGoAwayFrame.getStatus().getCode()); + } + + } else if (msg instanceof SpdyHeadersFrame) { + + SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(version, spdyHeadersFrame)); + byte flags = spdyHeadersFrame.isLast() ? SPDY_FLAG_FIN : 0; + int headerBlockLength = data.readableBytes(); + int length; + if (version < 3) { + length = headerBlockLength == 0 ? 4 : 6 + headerBlockLength; + } else { + length = 4 + headerBlockLength; + } + out.ensureWritableBytes(SPDY_HEADER_SIZE + length); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_HEADERS_FRAME); + out.writeByte(flags); + out.writeMedium(length); + out.writeInt(spdyHeadersFrame.getStreamID()); + if (version < 3 && headerBlockLength != 0) { + out.writeShort(0); + } + out.writeBytes(data, data.readerIndex(), headerBlockLength); + + } else if (msg instanceof SpdyWindowUpdateFrame) { + + SpdyWindowUpdateFrame spdyWindowUpdateFrame = (SpdyWindowUpdateFrame) msg; + out.ensureWritableBytes(SPDY_HEADER_SIZE + 8); + out.writeShort(version | 0x8000); + out.writeShort(SPDY_WINDOW_UPDATE_FRAME); + out.writeInt(8); + out.writeInt(spdyWindowUpdateFrame.getStreamID()); + out.writeInt(spdyWindowUpdateFrame.getDeltaWindowSize()); + } else { + throw new UnsupportedMessageTypeException(msg); + } + } + + private static void writeLengthField(int version, ChannelBuffer buffer, int length) { + if (version < 3) { + buffer.writeShort(length); + } else { + buffer.writeInt(length); + } + } + + private static void setLengthField(int version, ChannelBuffer buffer, int writerIndex, int length) { + if (version < 3) { + buffer.setShort(writerIndex, length); + } else { + buffer.setInt(writerIndex, length); + } + } + + private static ChannelBuffer encodeHeaderBlock(int version, 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); + writeLengthField(version, headerBlock, numHeaders); + for (String name: names) { + byte[] nameBytes = name.getBytes("UTF-8"); + writeLengthField(version, headerBlock, nameBytes.length); + headerBlock.writeBytes(nameBytes); + int savedIndex = headerBlock.writerIndex(); + int valueLength = 0; + writeLengthField(version, headerBlock, 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); + } + setLengthField(version, headerBlock, 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; + } + ChannelBuffer compressed = ChannelBuffers.dynamicBuffer(); + synchronized (headerBlockCompressor) { + if (!finished) { + headerBlockCompressor.setInput(uncompressed); + headerBlockCompressor.encode(compressed); + } + } + return compressed; + } +} 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..6abb5080a8 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyGoAwayFrame.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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); + + /** + * Returns the status of this frame. + */ + SpdySessionStatus getStatus(); + + /** + * Sets the status of this frame. + */ + void setStatus(SpdySessionStatus status); +} 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..5590ceaaf8 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlock.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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/SpdyHeaderBlockCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockCompressor.java new file mode 100644 index 0000000000..57f8060304 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockCompressor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.buffer.ChannelBuffer; +import io.netty.util.internal.DetectionUtil; + +abstract class SpdyHeaderBlockCompressor { + + static SpdyHeaderBlockCompressor newInstance( + int version, int compressionLevel, int windowBits, int memLevel) { + + if (DetectionUtil.javaVersion() >= 7) { + return new SpdyHeaderBlockZlibCompressor( + version, compressionLevel); + } else { + return new SpdyHeaderBlockJZlibCompressor( + version, compressionLevel, windowBits, memLevel); + } + } + + abstract void setInput(ChannelBuffer decompressed); + abstract void encode(ChannelBuffer compressed); + abstract void end(); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecompressor.java new file mode 100644 index 0000000000..b861249d83 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockDecompressor.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.buffer.ChannelBuffer; + +abstract class SpdyHeaderBlockDecompressor { + + static SpdyHeaderBlockDecompressor newInstance(int version) { + return new SpdyHeaderBlockZlibDecompressor(version); + } + + abstract void setInput(ChannelBuffer compressed); + abstract void decode(ChannelBuffer decompressed) throws Exception; + abstract void end(); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockJZlibCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockJZlibCompressor.java new file mode 100644 index 0000000000..b7236fab0a --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockJZlibCompressor.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + +import io.netty.buffer.ChannelBuffer; +import io.netty.handler.codec.compression.CompressionException; +import io.netty.util.internal.jzlib.JZlib; +import io.netty.util.internal.jzlib.ZStream; + +class SpdyHeaderBlockJZlibCompressor extends SpdyHeaderBlockCompressor { + + private final ZStream z = new ZStream(); + + public SpdyHeaderBlockJZlibCompressor( + int version, int compressionLevel, int windowBits, int memLevel) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } + if (compressionLevel < 0 || compressionLevel > 9) { + throw new IllegalArgumentException( + "compressionLevel: " + compressionLevel + " (expected: 0-9)"); + } + if (windowBits < 9 || windowBits > 15) { + throw new IllegalArgumentException( + "windowBits: " + windowBits + " (expected: 9-15)"); + } + if (memLevel < 1 || memLevel > 9) { + throw new IllegalArgumentException( + "memLevel: " + memLevel + " (expected: 1-9)"); + } + + int resultCode = z.deflateInit( + compressionLevel, windowBits, memLevel, JZlib.W_ZLIB); + if (resultCode != JZlib.Z_OK) { + throw new CompressionException( + "failed to initialize an SPDY header block deflater: " + resultCode); + } else { + if (version < 3) { + resultCode = z.deflateSetDictionary(SPDY2_DICT, SPDY2_DICT.length); + } else { + resultCode = z.deflateSetDictionary(SPDY_DICT, SPDY_DICT.length); + } + if (resultCode != JZlib.Z_OK) { + throw new CompressionException( + "failed to set the SPDY dictionary: " + resultCode); + } + } + } + + @Override + public void setInput(ChannelBuffer decompressed) { + byte[] in = new byte[decompressed.readableBytes()]; + decompressed.readBytes(in); + z.next_in = in; + z.next_in_index = 0; + z.avail_in = in.length; + } + + @Override + public void encode(ChannelBuffer compressed) { + try { + byte[] out = new byte[(int) Math.ceil(z.next_in.length * 1.001) + 12]; + z.next_out = out; + z.next_out_index = 0; + z.avail_out = out.length; + + int resultCode = z.deflate(JZlib.Z_SYNC_FLUSH); + if (resultCode != JZlib.Z_OK) { + throw new CompressionException("compression failure: " + resultCode); + } + + if (z.next_out_index != 0) { + compressed.writeBytes(out, 0, z.next_out_index); + } + } finally { + // Deference the external references explicitly to tell the VM that + // the allocated byte arrays are temporary so that the call stack + // can be utilized. + // I'm not sure if the modern VMs do this optimization though. + z.next_in = null; + z.next_out = null; + } + } + + @Override + public void end() { + z.deflateEnd(); + z.next_in = null; + z.next_out = null; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibCompressor.java new file mode 100644 index 0000000000..dad8ee647f --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibCompressor.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import java.util.zip.Deflater; + +import io.netty.buffer.ChannelBuffer; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + +class SpdyHeaderBlockZlibCompressor extends SpdyHeaderBlockCompressor { + + private final byte[] out = new byte[8192]; + private final Deflater compressor; + + public SpdyHeaderBlockZlibCompressor(int version, int compressionLevel) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } + if (compressionLevel < 0 || compressionLevel > 9) { + throw new IllegalArgumentException( + "compressionLevel: " + compressionLevel + " (expected: 0-9)"); + } + compressor = new Deflater(compressionLevel); + if (version < 3) { + compressor.setDictionary(SPDY2_DICT); + } else { + compressor.setDictionary(SPDY_DICT); + } + } + + @Override + public void setInput(ChannelBuffer decompressed) { + byte[] in = new byte[decompressed.readableBytes()]; + decompressed.readBytes(in); + compressor.setInput(in); + } + + @Override + public void encode(ChannelBuffer compressed) { + while (!compressor.needsInput()) { + int numBytes = compressor.deflate(out, 0, out.length, Deflater.SYNC_FLUSH); + compressed.writeBytes(out, 0, numBytes); + } + } + + @Override + public void end() { + compressor.end(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecompressor.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecompressor.java new file mode 100644 index 0000000000..1fa8e7b038 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibDecompressor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import io.netty.buffer.ChannelBuffer; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; + +class SpdyHeaderBlockZlibDecompressor extends SpdyHeaderBlockDecompressor { + + private final int version; + private final byte[] out = new byte[8192]; + private final Inflater decompressor = new Inflater(); + + public SpdyHeaderBlockZlibDecompressor(int version) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } + this.version = version; + } + + @Override + public void setInput(ChannelBuffer compressed) { + byte[] in = new byte[compressed.readableBytes()]; + compressed.readBytes(in); + decompressor.setInput(in); + } + + @Override + public void decode(ChannelBuffer decompressed) throws Exception { + try { + int numBytes = decompressor.inflate(out); + if (numBytes == 0 && decompressor.needsDictionary()) { + if (version < 3) { + decompressor.setDictionary(SPDY2_DICT); + } else { + decompressor.setDictionary(SPDY_DICT); + } + numBytes = decompressor.inflate(out); + } + decompressed.writeBytes(out, 0, numBytes); + } catch (DataFormatException e) { + throw new SpdyProtocolException( + "Received invalid header block", e); + } + } + + @Override + public void end() { + decompressor.end(); + } +} 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..0f5ef6bbeb --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaders.java @@ -0,0 +1,770 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * Provides the constants for the standard SPDY HTTP header names and commonly + * used utility methods that access a {@link SpdyHeaderBlock}. + * @apiviz.stereotype static + */ +public class SpdyHeaders { + + /** + * SPDY HTTP header names + * @apiviz.stereotype static + */ + public static final class HttpNames { + /** + * {@code ":host"} + */ + public static final String HOST = ":host"; + /** + * {@code ":method"} + */ + public static final String METHOD = ":method"; + /** + * {@code ":path"} + */ + public static final String PATH = ":path"; + /** + * {@code ":scheme"} + */ + public static final String SCHEME = ":scheme"; + /** + * {@code ":status"} + */ + public static final String STATUS = ":status"; + /** + * {@code ":version"} + */ + public static final String VERSION = ":version"; + + private HttpNames() { + super(); + } + } + + /** + * SPDY/2 HTTP header names + * @apiviz.stereotype static + */ + public static final class Spdy2HttpNames { + /** + * {@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 Spdy2HttpNames() { + 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 SPDY host header. + */ + public static void removeHost(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.HOST); + } + + /** + * Returns the SPDY host header. + */ + public static String getHost(SpdyHeaderBlock block) { + return block.getHeader(HttpNames.HOST); + } + + /** + * Set the SPDY host header. + */ + public static void setHost(SpdyHeaderBlock block, String host) { + block.setHeader(HttpNames.HOST, host); + } + + /** + * Removes the HTTP method header. + */ + @Deprecated + public static void removeMethod(SpdyHeaderBlock block) { + removeMethod(2, block); + } + + /** + * Removes the HTTP method header. + */ + public static void removeMethod(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + block.removeHeader(Spdy2HttpNames.METHOD); + } else { + block.removeHeader(HttpNames.METHOD); + } + } + + /** + * Returns the {@link HttpMethod} represented by the HTTP method header. + */ + @Deprecated + public static HttpMethod getMethod(SpdyHeaderBlock block) { + return getMethod(2, block); + } + + /** + * Returns the {@link HttpMethod} represented by the HTTP method header. + */ + public static HttpMethod getMethod(int spdyVersion, SpdyHeaderBlock block) { + try { + if (spdyVersion < 3) { + return HttpMethod.valueOf(block.getHeader(Spdy2HttpNames.METHOD)); + } else { + return HttpMethod.valueOf(block.getHeader(HttpNames.METHOD)); + } + } catch (Exception e) { + return null; + } + } + + /** + * Sets the HTTP method header. + */ + @Deprecated + public static void setMethod(SpdyHeaderBlock block, HttpMethod method) { + setMethod(2, block, method); + } + + /** + * Sets the HTTP method header. + */ + public static void setMethod(int spdyVersion, SpdyHeaderBlock block, HttpMethod method) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.METHOD, method.getName()); + } else { + block.setHeader(HttpNames.METHOD, method.getName()); + } + } + + /** + * Removes the URL scheme header. + */ + @Deprecated + public static void removeScheme(SpdyHeaderBlock block) { + removeMethod(2, block); + } + + /** + * Removes the URL scheme header. + */ + public static void removeScheme(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 2) { + block.removeHeader(Spdy2HttpNames.SCHEME); + } else { + block.removeHeader(HttpNames.SCHEME); + } + } + + /** + * Returns the value of the URL scheme header. + */ + @Deprecated + public static String getScheme(SpdyHeaderBlock block) { + return getScheme(2, block); + } + + /** + * Returns the value of the URL scheme header. + */ + public static String getScheme(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + return block.getHeader(Spdy2HttpNames.SCHEME); + } else { + return block.getHeader(HttpNames.SCHEME); + } + } + + /** + * Sets the URL scheme header. + */ + @Deprecated + public static void setScheme(SpdyHeaderBlock block, String scheme) { + setScheme(2, block, scheme); + } + + /** + * Sets the URL scheme header. + */ + public static void setScheme(int spdyVersion, SpdyHeaderBlock block, String scheme) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.SCHEME, scheme); + } else { + block.setHeader(HttpNames.SCHEME, scheme); + } + } + + /** + * Removes the HTTP response status header. + */ + @Deprecated + public static void removeStatus(SpdyHeaderBlock block) { + removeMethod(2, block); + } + + /** + * Removes the HTTP response status header. + */ + public static void removeStatus(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + block.removeHeader(Spdy2HttpNames.STATUS); + } else { + block.removeHeader(HttpNames.STATUS); + } + } + + /** + * Returns the {@link HttpResponseStatus} represented by the HTTP response status header. + */ + @Deprecated + public static HttpResponseStatus getStatus(SpdyHeaderBlock block) { + return getStatus(2, block); + } + + /** + * Returns the {@link HttpResponseStatus} represented by the HTTP response status header. + */ + public static HttpResponseStatus getStatus(int spdyVersion, SpdyHeaderBlock block) { + try { + String status; + if (spdyVersion < 3) { + status = block.getHeader(Spdy2HttpNames.STATUS); + } else { + 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(reasonPhrase)) { + return responseStatus; + } else { + return new HttpResponseStatus(code, reasonPhrase); + } + } + } catch (Exception e) { + return null; + } + } + + /** + * Sets the HTTP response status header. + */ + @Deprecated + public static void setStatus(SpdyHeaderBlock block, HttpResponseStatus status) { + setStatus(2, block, status); + } + + /** + * Sets the HTTP response status header. + */ + public static void setStatus(int spdyVersion, SpdyHeaderBlock block, HttpResponseStatus status) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.STATUS, status.toString()); + } else { + block.setHeader(HttpNames.STATUS, status.toString()); + } + } + + /** + * Removes the URL path header. + */ + @Deprecated + public static void removeUrl(SpdyHeaderBlock block) { + removeUrl(2, block); + } + + /** + * Removes the URL path header. + */ + public static void removeUrl(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + block.removeHeader(Spdy2HttpNames.URL); + } else { + block.removeHeader(HttpNames.PATH); + } + } + + /** + * Returns the value of the URL path header. + */ + @Deprecated + public static String getUrl(SpdyHeaderBlock block) { + return getUrl(2, block); + } + + /** + * Returns the value of the URL path header. + */ + public static String getUrl(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + return block.getHeader(Spdy2HttpNames.URL); + } else { + return block.getHeader(HttpNames.PATH); + } + } + + /** + * Sets the URL path header. + */ + @Deprecated + public static void setUrl(SpdyHeaderBlock block, String path) { + setUrl(2, block, path); + } + + /** + * Sets the URL path header. + */ + public static void setUrl(int spdyVersion, SpdyHeaderBlock block, String path) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.URL, path); + } else { + block.setHeader(HttpNames.PATH, path); + } + } + + /** + * Removes the HTTP version header. + */ + @Deprecated + public static void removeVersion(SpdyHeaderBlock block) { + removeVersion(2, block); + } + + /** + * Removes the HTTP version header. + */ + public static void removeVersion(int spdyVersion, SpdyHeaderBlock block) { + if (spdyVersion < 3) { + block.removeHeader(Spdy2HttpNames.VERSION); + } else { + block.removeHeader(HttpNames.VERSION); + } + } + + /** + * Returns the {@link HttpVersion} represented by the HTTP version header. + */ + @Deprecated + public static HttpVersion getVersion(SpdyHeaderBlock block) { + return getVersion(2, block); + } + + /** + * Returns the {@link HttpVersion} represented by the HTTP version header. + */ + public static HttpVersion getVersion(int spdyVersion, SpdyHeaderBlock block) { + try { + if (spdyVersion < 3) { + return HttpVersion.valueOf(block.getHeader(Spdy2HttpNames.VERSION)); + } else { + return HttpVersion.valueOf(block.getHeader(HttpNames.VERSION)); + } + } catch (Exception e) { + return null; + } + } + + /** + * Sets the HTTP version header. + */ + @Deprecated + public static void setVersion(SpdyHeaderBlock block, HttpVersion httpVersion) { + setVersion(2, block, httpVersion); + } + + /** + * Sets the HTTP version header. + */ + public static void setVersion(int spdyVersion, SpdyHeaderBlock block, HttpVersion httpVersion) { + if (spdyVersion < 3) { + block.setHeader(Spdy2HttpNames.VERSION, httpVersion.getText()); + } else { + block.setHeader(HttpNames.VERSION, httpVersion.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; + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getValue() { + return value; + } + + @Override + 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..f136f378d7 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeadersFrame.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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); + + /** + * 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/SpdyHttpCodec.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpCodec.java new file mode 100644 index 0000000000..e43fefcfc8 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpCodec.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.channel.CombinedChannelHandler; + +/** + * A combination of {@link SpdyHttpDecoder} and {@link SpdyHttpEncoder} + * @apiviz.has io.netty.handler.codec.sdpy.SpdyHttpDecoder + * @apiviz.has io.netty.handler.codec.spdy.SpdyHttpEncoder + */ +public class SpdyHttpCodec extends CombinedChannelHandler { + + /** + * Creates a new instance with the specified decoder options. + */ + public SpdyHttpCodec(int version, int maxContentLength) { + super( + new SpdyHttpDecoder(version, maxContentLength), + new SpdyHttpEncoder(version)); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java new file mode 100644 index 0000000000..a3085bc49f --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java @@ -0,0 +1,291 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; +import io.netty.channel.ChannelInboundHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +import java.util.HashMap; +import java.util.Map; + +/** + * Decodes {@link SpdySynStreamFrame}s, {@link SpdySynReplyFrame}s, + * and {@link SpdyDataFrame}s into {@link HttpRequest}s and {@link HttpResponse}s. + */ +public class SpdyHttpDecoder extends MessageToMessageDecoder { + + private final int spdyVersion; + private final int maxContentLength; + private final Map messageMap = new HashMap(); + + /** + * Creates a new instance. + * + * @param version the protocol version + * @param maxContentLength the maximum length of the message content. + * If the length of the message content exceeds this value, + * a {@link TooLongFrameException} will be raised. + */ + public SpdyHttpDecoder(int version, int maxContentLength) { + super(); + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } + if (maxContentLength <= 0) { + throw new IllegalArgumentException( + "maxContentLength must be a positive integer: " + maxContentLength); + } + spdyVersion = version; + this.maxContentLength = maxContentLength; + } + + @Override + public HttpMessage decode(ChannelInboundHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof SpdySynStreamFrame) { + + // HTTP requests/responses are mapped one-to-one to SPDY streams. + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + int streamID = spdySynStreamFrame.getStreamID(); + + if (SpdyCodecUtil.isServerID(streamID)) { + // SYN_STREAM frames initiated by the server are pushed resources + int associatedToStreamID = spdySynStreamFrame.getAssociatedToStreamID(); + + // If a client receives a SYN_STREAM with an Associated-To-Stream-ID of 0 + // it must reply with a RST_STREAM with error code INVALID_STREAM + if (associatedToStreamID == 0) { + SpdyRstStreamFrame spdyRstStreamFrame = + new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.INVALID_STREAM); + ctx.write(spdyRstStreamFrame); + } + + String URL = SpdyHeaders.getUrl(spdyVersion, spdySynStreamFrame); + + // If a client receives a SYN_STREAM without a 'url' header + // it must reply with a RST_STREAM with error code PROTOCOL_ERROR + if (URL == null) { + SpdyRstStreamFrame spdyRstStreamFrame = + new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR); + ctx.write(spdyRstStreamFrame); + } + + try { + HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynStreamFrame); + + // Set the Stream-ID, Associated-To-Stream-ID, Priority, and URL as headers + SpdyHttpHeaders.setStreamID(httpResponse, streamID); + SpdyHttpHeaders.setAssociatedToStreamID(httpResponse, associatedToStreamID); + SpdyHttpHeaders.setPriority(httpResponse, spdySynStreamFrame.getPriority()); + SpdyHttpHeaders.setUrl(httpResponse, URL); + + if (spdySynStreamFrame.isLast()) { + HttpHeaders.setContentLength(httpResponse, 0); + return httpResponse; + } else { + // Response body will follow in a series of Data Frames + messageMap.put(new Integer(streamID), httpResponse); + } + } catch (Exception e) { + SpdyRstStreamFrame spdyRstStreamFrame = + new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR); + ctx.write(spdyRstStreamFrame); + } + + } else { + // SYN_STREAM frames initiated by the client are HTTP requests + try { + HttpRequest httpRequest = createHttpRequest(spdyVersion, spdySynStreamFrame); + + // Set the Stream-ID as a header + SpdyHttpHeaders.setStreamID(httpRequest, streamID); + + if (spdySynStreamFrame.isLast()) { + return httpRequest; + } else { + // Request body will follow in a series of Data Frames + messageMap.put(new Integer(streamID), httpRequest); + } + } catch (Exception e) { + // If a client sends a SYN_STREAM without all of the method, url (host and path), + // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply. + // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid + SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID); + spdySynReplyFrame.setLast(true); + SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, HttpResponseStatus.BAD_REQUEST); + SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, HttpVersion.HTTP_1_0); + ctx.write(spdySynReplyFrame); + } + } + + } else if (msg instanceof SpdySynReplyFrame) { + + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + int streamID = spdySynReplyFrame.getStreamID(); + + try { + HttpResponse httpResponse = createHttpResponse(spdyVersion, spdySynReplyFrame); + + // Set the Stream-ID as a header + SpdyHttpHeaders.setStreamID(httpResponse, streamID); + + if (spdySynReplyFrame.isLast()) { + HttpHeaders.setContentLength(httpResponse, 0); + return httpResponse; + } else { + // Response body will follow in a series of Data Frames + messageMap.put(new Integer(streamID), httpResponse); + } + } catch (Exception e) { + // If a client receives a SYN_REPLY without valid status and version headers + // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR + SpdyRstStreamFrame spdyRstStreamFrame = + new DefaultSpdyRstStreamFrame(streamID, SpdyStreamStatus.PROTOCOL_ERROR); + ctx.write(spdyRstStreamFrame); + } + + } else if (msg instanceof SpdyHeadersFrame) { + + SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; + Integer streamID = new Integer(spdyHeadersFrame.getStreamID()); + HttpMessage httpMessage = messageMap.get(streamID); + + // If message is not in map discard HEADERS frame. + // SpdySessionHandler should prevent this from happening. + if (httpMessage == null) { + return null; + } + + for (Map.Entry e: spdyHeadersFrame.getHeaders()) { + httpMessage.addHeader(e.getKey(), e.getValue()); + } + + } else if (msg instanceof SpdyDataFrame) { + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + Integer streamID = new Integer(spdyDataFrame.getStreamID()); + HttpMessage httpMessage = messageMap.get(streamID); + + // If message is not in map discard Data Frame. + // SpdySessionHandler should prevent this from happening. + if (httpMessage == null) { + return null; + } + + ChannelBuffer content = httpMessage.getContent(); + if (content.readableBytes() > maxContentLength - spdyDataFrame.getData().readableBytes()) { + messageMap.remove(streamID); + throw new TooLongFrameException( + "HTTP content length exceeded " + maxContentLength + " bytes."); + } + + ChannelBuffer spdyDataFrameData = spdyDataFrame.getData(); + int spdyDataFrameDataLen = spdyDataFrameData.readableBytes(); + if (content == ChannelBuffers.EMPTY_BUFFER) { + content = ChannelBuffers.dynamicBuffer(spdyDataFrameDataLen); + content.writeBytes(spdyDataFrameData, spdyDataFrameData.readerIndex(), spdyDataFrameDataLen); + httpMessage.setContent(content); + } else { + content.writeBytes(spdyDataFrameData, spdyDataFrameData.readerIndex(), spdyDataFrameDataLen); + } + + if (spdyDataFrame.isLast()) { + HttpHeaders.setContentLength(httpMessage, content.readableBytes()); + messageMap.remove(streamID); + return httpMessage; + } + + } else if (msg instanceof SpdyRstStreamFrame) { + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + Integer streamID = new Integer(spdyRstStreamFrame.getStreamID()); + messageMap.remove(streamID); + } + + return null; + } + + private static HttpRequest createHttpRequest(int spdyVersion, SpdyHeaderBlock requestFrame) + throws Exception { + // Create the first line of the request from the name/value pairs + HttpMethod method = SpdyHeaders.getMethod(spdyVersion, requestFrame); + String url = SpdyHeaders.getUrl(spdyVersion, requestFrame); + HttpVersion httpVersion = SpdyHeaders.getVersion(spdyVersion, requestFrame); + SpdyHeaders.removeMethod(spdyVersion, requestFrame); + SpdyHeaders.removeUrl(spdyVersion, requestFrame); + SpdyHeaders.removeVersion(spdyVersion, requestFrame); + + HttpRequest httpRequest = new DefaultHttpRequest(httpVersion, method, url); + + // Remove the scheme header + SpdyHeaders.removeScheme(spdyVersion, requestFrame); + + if (spdyVersion >= 3) { + // Replace the SPDY host header with the HTTP host header + String host = SpdyHeaders.getHost(requestFrame); + SpdyHeaders.removeHost(requestFrame); + HttpHeaders.setHost(httpRequest, host); + } + + for (Map.Entry e: requestFrame.getHeaders()) { + httpRequest.addHeader(e.getKey(), e.getValue()); + } + + // The Connection and Keep-Alive headers are no longer valid + HttpHeaders.setKeepAlive(httpRequest, true); + + // Transfer-Encoding header is not valid + httpRequest.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); + + return httpRequest; + } + + private static HttpResponse createHttpResponse(int spdyVersion, SpdyHeaderBlock responseFrame) + throws Exception { + // Create the first line of the response from the name/value pairs + HttpResponseStatus status = SpdyHeaders.getStatus(spdyVersion, responseFrame); + HttpVersion version = SpdyHeaders.getVersion(spdyVersion, responseFrame); + SpdyHeaders.removeStatus(spdyVersion, responseFrame); + SpdyHeaders.removeVersion(spdyVersion, responseFrame); + + HttpResponse httpResponse = new DefaultHttpResponse(version, status); + for (Map.Entry e: responseFrame.getHeaders()) { + httpResponse.addHeader(e.getKey(), e.getValue()); + } + + // The Connection and Keep-Alive headers are no longer valid + HttpHeaders.setKeepAlive(httpResponse, true); + + // Transfer-Encoding header is not valid + httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); + httpResponse.removeHeader(HttpHeaders.Names.TRAILER); + + return httpResponse; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java new file mode 100644 index 0000000000..1b1b75807a --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java @@ -0,0 +1,322 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; +import io.netty.channel.ChannelOutboundHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.UnsupportedMessageTypeException; +import io.netty.handler.codec.http.HttpChunk; +import io.netty.handler.codec.http.HttpChunkTrailer; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Encodes {@link HttpRequest}s, {@link HttpResponse}s, and {@link HttpChunk}s + * into {@link SpdySynStreamFrame}s and {@link SpdySynReplyFrame}s. + * + *

Request Annotations

+ * + * SPDY specific headers must be added to {@link HttpRequest}s: + * + * + * + * + * + * + * + * + * + * + * + * + *
Header NameHeader Value
{@code "X-SPDY-Stream-ID"}The Stream-ID for this request. + * Stream-IDs must be odd, positive integers, and must increase monotonically.
{@code "X-SPDY-Priority"}The priority value for this request. + * The priority should be between 0 and 7 inclusive. + * 0 represents the highest priority and 7 represents the lowest. + * This header is optional and defaults to 0.
+ * + *

Response Annotations

+ * + * SPDY specific headers must be added to {@link HttpResponse}s: + * + * + * + * + * + * + * + * + *
Header NameHeader Value
{@code "X-SPDY-Stream-ID"}The Stream-ID of the request corresponding to this response.
+ * + *

Pushed Resource Annotations

+ * + * SPDY specific headers must be added to pushed {@link HttpResponse}s: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Header NameHeader Value
{@code "X-SPDY-Stream-ID"}The Stream-ID for this resource. + * Stream-IDs must be even, positive integers, and must increase monotonically.
{@code "X-SPDY-Associated-To-Stream-ID"}The Stream-ID of the request that initiated this pushed resource.
{@code "X-SPDY-Priority"}The priority value for this resource. + * The priority should be between 0 and 7 inclusive. + * 0 represents the highest priority and 7 represents the lowest. + * This header is optional and defaults to 0.
{@code "X-SPDY-URL"}The absolute path for the resource being pushed.
+ * + *

Required Annotations

+ * + * SPDY requires that all Requests and Pushed Resources contain + * an HTTP "Host" header. + * + *

Optional Annotations

+ * + * Requests and Pushed Resources must contain a SPDY scheme header. + * This can be set via the {@code "X-SPDY-Scheme"} header but otherwise + * defaults to "https" as that is the most common SPDY deployment. + * + *

Chunked Content

+ * + * This encoder associates all {@link HttpChunk}s that it receives + * with the most recently received 'chunked' {@link HttpRequest} + * or {@link HttpResponse}. + * + *

Pushed Resources

+ * + * All pushed resources should be sent before sending the response + * that corresponds to the initial request. + */ +public class SpdyHttpEncoder extends MessageToMessageEncoder { + + private final int spdyVersion; + private volatile int currentStreamID; + + /** + * Creates a new instance. + * + * @param version the protocol version + */ + public SpdyHttpEncoder(int version) { + if (version < SPDY_MIN_VERSION || version > SPDY_MAX_VERSION) { + throw new IllegalArgumentException( + "unsupported version: " + version); + } + spdyVersion = version; + } + + @Override + public boolean isEncodable(Object msg) throws Exception { + return msg instanceof HttpRequest || + msg instanceof HttpResponse || + msg instanceof HttpChunk; + } + + @Override + public Object encode(ChannelOutboundHandlerContext ctx, Object msg) throws Exception { + + List out = new ArrayList(); + if (msg instanceof HttpRequest) { + + HttpRequest httpRequest = (HttpRequest) msg; + SpdySynStreamFrame spdySynStreamFrame = createSynStreamFrame(httpRequest); + int streamID = spdySynStreamFrame.getStreamID(); + out.add(spdySynStreamFrame); + addContent(out, streamID, httpRequest); + + } else if (msg instanceof HttpResponse) { + + HttpResponse httpResponse = (HttpResponse) msg; + if (httpResponse.containsHeader(SpdyHttpHeaders.Names.ASSOCIATED_TO_STREAM_ID)) { + SpdySynStreamFrame spdySynStreamFrame = createSynStreamFrame(httpResponse); + int streamID = spdySynStreamFrame.getStreamID(); + out.add(spdySynStreamFrame); + addContent(out, streamID, httpResponse); + } else { + SpdySynReplyFrame spdySynReplyFrame = createSynReplyFrame(httpResponse); + int streamID = spdySynReplyFrame.getStreamID(); + out.add(spdySynReplyFrame); + addContent(out, streamID, httpResponse); + } + + } else if (msg instanceof HttpChunk) { + + HttpChunk chunk = (HttpChunk) msg; + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(currentStreamID); + spdyDataFrame.setData(chunk.getContent()); + spdyDataFrame.setLast(chunk.isLast()); + + if (chunk instanceof HttpChunkTrailer) { + HttpChunkTrailer trailer = (HttpChunkTrailer) chunk; + List> trailers = trailer.getHeaders(); + if (trailers.isEmpty()) { + out.add(spdyDataFrame); + } else { + // Create SPDY HEADERS frame out of trailers + SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(currentStreamID); + for (Map.Entry entry: trailers) { + spdyHeadersFrame.addHeader(entry.getKey(), entry.getValue()); + } + + // Write HEADERS frame and append Data Frame + out.add(spdyHeadersFrame); + out.add(spdyDataFrame); + } + } else { + out.add(spdyDataFrame); + } + } else { + throw new UnsupportedMessageTypeException(msg); + } + + return out.toArray(); + } + + private static void addContent(List out, int streamID, HttpMessage httpMessage) { + if (!httpMessage.getContent().readable()) { + return; + } + + // Create SPDY Data Frame out of message content + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID); + spdyDataFrame.setData(httpMessage.getContent()); + spdyDataFrame.setLast(true); + + out.add(spdyDataFrame); + } + + private SpdySynStreamFrame createSynStreamFrame(HttpMessage httpMessage) + throws Exception { + boolean chunked = httpMessage.isChunked(); + + // Get the Stream-ID, Associated-To-Stream-ID, Priority, URL, and scheme from the headers + int streamID = SpdyHttpHeaders.getStreamID(httpMessage); + int associatedToStreamID = SpdyHttpHeaders.getAssociatedToStreamID(httpMessage); + byte priority = SpdyHttpHeaders.getPriority(httpMessage); + String URL = SpdyHttpHeaders.getUrl(httpMessage); + String scheme = SpdyHttpHeaders.getScheme(httpMessage); + SpdyHttpHeaders.removeStreamID(httpMessage); + SpdyHttpHeaders.removeAssociatedToStreamID(httpMessage); + SpdyHttpHeaders.removePriority(httpMessage); + SpdyHttpHeaders.removeUrl(httpMessage); + SpdyHttpHeaders.removeScheme(httpMessage); + + // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding + // headers are not valid and MUST not be sent. + httpMessage.removeHeader(HttpHeaders.Names.CONNECTION); + httpMessage.removeHeader("Keep-Alive"); + httpMessage.removeHeader("Proxy-Connection"); + httpMessage.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); + + SpdySynStreamFrame spdySynStreamFrame = new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority); + + // Unfold the first line of the message into name/value pairs + if (httpMessage instanceof HttpRequest) { + HttpRequest httpRequest = (HttpRequest) httpMessage; + SpdyHeaders.setMethod(spdyVersion, spdySynStreamFrame, httpRequest.getMethod()); + SpdyHeaders.setUrl(spdyVersion, spdySynStreamFrame, httpRequest.getUri()); + SpdyHeaders.setVersion(spdyVersion, spdySynStreamFrame, httpMessage.getProtocolVersion()); + } + if (httpMessage instanceof HttpResponse) { + HttpResponse httpResponse = (HttpResponse) httpMessage; + SpdyHeaders.setStatus(spdyVersion, spdySynStreamFrame, httpResponse.getStatus()); + SpdyHeaders.setUrl(spdyVersion, spdySynStreamFrame, URL); + spdySynStreamFrame.setUnidirectional(true); + } + + // Replace the HTTP host header with the SPDY host header + if (spdyVersion >= 3) { + String host = HttpHeaders.getHost(httpMessage); + httpMessage.removeHeader(HttpHeaders.Names.HOST); + SpdyHeaders.setHost(spdySynStreamFrame, host); + } + + // Set the SPDY scheme header + if (scheme == null) { + scheme = "https"; + } + SpdyHeaders.setScheme(spdyVersion, spdySynStreamFrame, scheme); + + // Transfer the remaining HTTP headers + for (Map.Entry entry: httpMessage.getHeaders()) { + spdySynStreamFrame.addHeader(entry.getKey(), entry.getValue()); + } + + if (chunked) { + currentStreamID = streamID; + spdySynStreamFrame.setLast(false); + } else { + spdySynStreamFrame.setLast(httpMessage.getContent().readableBytes() == 0); + } + + return spdySynStreamFrame; + } + + private SpdySynReplyFrame createSynReplyFrame(HttpResponse httpResponse) + throws Exception { + boolean chunked = httpResponse.isChunked(); + + // Get the Stream-ID from the headers + int streamID = SpdyHttpHeaders.getStreamID(httpResponse); + SpdyHttpHeaders.removeStreamID(httpResponse); + + // The Connection, Keep-Alive, Proxy-Connection, and Transfer-ENcoding + // headers are not valid and MUST not be sent. + httpResponse.removeHeader(HttpHeaders.Names.CONNECTION); + httpResponse.removeHeader("Keep-Alive"); + httpResponse.removeHeader("Proxy-Connection"); + httpResponse.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); + + SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamID); + + // Unfold the first line of the response into name/value pairs + SpdyHeaders.setStatus(spdyVersion, spdySynReplyFrame, httpResponse.getStatus()); + SpdyHeaders.setVersion(spdyVersion, spdySynReplyFrame, httpResponse.getProtocolVersion()); + + // Transfer the remaining HTTP headers + for (Map.Entry entry: httpResponse.getHeaders()) { + spdySynReplyFrame.addHeader(entry.getKey(), entry.getValue()); + } + + if (chunked) { + currentStreamID = streamID; + spdySynReplyFrame.setLast(false); + } else { + spdySynReplyFrame.setLast(httpResponse.getContent().readableBytes() == 0); + } + + return spdySynReplyFrame; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpHeaders.java new file mode 100644 index 0000000000..005a2e4c90 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpHeaders.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; + +/** + * Provides the constants for the header names and the utility methods + * used by the {@link SpdyHttpDecoder} and {@link SpdyHttpEncoder}. + * @apiviz.stereotype static + */ +public final class SpdyHttpHeaders { + + /** + * SPDY HTTP header names + * @apiviz.stereotype static + */ + public static final class Names { + /** + * {@code "X-SPDY-Stream-ID"} + */ + public static final String STREAM_ID = "X-SPDY-Stream-ID"; + /** + * {@code "X-SPDY-Associated-To-Stream-ID"} + */ + public static final String ASSOCIATED_TO_STREAM_ID = "X-SPDY-Associated-To-Stream-ID"; + /** + * {@code "X-SPDY-Priority"} + */ + public static final String PRIORITY = "X-SPDY-Priority"; + /** + * {@code "X-SPDY-URL"} + */ + public static final String URL = "X-SPDY-URL"; + /** + * {@code "X-SPDY-Scheme"} + */ + public static final String SCHEME = "X-SPDY-Scheme"; + + private Names() { + super(); + } + } + + private SpdyHttpHeaders() { + } + + /** + * Removes the {@code "X-SPDY-Stream-ID"} header. + */ + public static void removeStreamID(HttpMessage message) { + message.removeHeader(Names.STREAM_ID); + } + + /** + * Returns the value of the {@code "X-SPDY-Stream-ID"} header. + */ + public static int getStreamID(HttpMessage message) { + return HttpHeaders.getIntHeader(message, Names.STREAM_ID); + } + + /** + * Sets the {@code "X-SPDY-Stream-ID"} header. + */ + public static void setStreamID(HttpMessage message, int streamID) { + HttpHeaders.setIntHeader(message, Names.STREAM_ID, streamID); + } + + /** + * Removes the {@code "X-SPDY-Associated-To-Stream-ID"} header. + */ + public static void removeAssociatedToStreamID(HttpMessage message) { + message.removeHeader(Names.ASSOCIATED_TO_STREAM_ID); + } + + /** + * Returns the value of the {@code "X-SPDY-Associated-To-Stream-ID"} header. + * + * @return the header value or {@code 0} if there is no such header or + * if the header value is not a number + */ + public static int getAssociatedToStreamID(HttpMessage message) { + return HttpHeaders.getIntHeader(message, Names.ASSOCIATED_TO_STREAM_ID, 0); + } + + /** + * Sets the {@code "X-SPDY-Associated-To-Stream-ID"} header. + */ + public static void setAssociatedToStreamID(HttpMessage message, int associatedToStreamID) { + HttpHeaders.setIntHeader(message, Names.ASSOCIATED_TO_STREAM_ID, associatedToStreamID); + } + + /** + * Removes the {@code "X-SPDY-Priority"} header. + */ + public static void removePriority(HttpMessage message) { + message.removeHeader(Names.PRIORITY); + } + + /** + * Returns the value of the {@code "X-SPDY-Priority"} header. + * + * @return the header value or {@code 0} if there is no such header or + * if the header value is not a number + */ + public static byte getPriority(HttpMessage message) { + return (byte) HttpHeaders.getIntHeader(message, Names.PRIORITY, 0); + } + + /** + * Sets the {@code "X-SPDY-Priority"} header. + */ + public static void setPriority(HttpMessage message, byte priority) { + HttpHeaders.setIntHeader(message, Names.PRIORITY, priority); + } + + /** + * Removes the {@code "X-SPDY-URL"} header. + */ + public static void removeUrl(HttpMessage message) { + message.removeHeader(Names.URL); + } + + /** + * Returns the value of the {@code "X-SPDY-URL"} header. + */ + public static String getUrl(HttpMessage message) { + return message.getHeader(Names.URL); + } + + /** + * Sets the {@code "X-SPDY-URL"} header. + */ + public static void setUrl(HttpMessage message, String url) { + message.setHeader(Names.URL, url); + } + + /** + * Removes the {@code "X-SPDY-Scheme"} header. + */ + public static void removeScheme(HttpMessage message) { + message.removeHeader(Names.SCHEME); + } + + /** + * Returns the value of the {@code "X-SPDY-Scheme"} header. + */ + public static String getScheme(HttpMessage message) { + return message.getHeader(Names.SCHEME); + } + + /** + * Sets the {@code "X-SPDY-Scheme"} header. + */ + public static void setScheme(HttpMessage message, String scheme) { + message.setHeader(Names.URL, scheme); + } +} 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..f417abd39f --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyNoOpFrame.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +/** + * A SPDY Protocol NOOP Control Frame + */ +public interface SpdyNoOpFrame { + // Tag interface +} 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..0dbfdb978b --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyPingFrame.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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..497fc0240e --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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 { + + private static final long serialVersionUID = 7870000537743847264L; + + /** + * 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..6b7eb71964 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyRstStreamFrame.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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/SpdySessionStatus.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java new file mode 100644 index 0000000000..3ff57d30ef --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +/** + * The SPDY session status code and its description. + * @apiviz.exclude + */ +public class SpdySessionStatus implements Comparable { + + /** + * 0 OK + */ + public static final SpdySessionStatus OK = + new SpdySessionStatus(0, "OK"); + + /** + * 1 Protocol Error + */ + public static final SpdySessionStatus PROTOCOL_ERROR = + new SpdySessionStatus(1, "PROTOCOL_ERROR"); + + /** + * 11 Internal Error + */ + public static final SpdySessionStatus INTERNAL_ERROR = + new SpdySessionStatus(11, "INTERNAL_ERROR"); + + /** + * Returns the {@link SpdySessionStatus} 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 SpdySessionStatus valueOf(int code) { + switch (code) { + case 0: + return OK; + case 1: + return PROTOCOL_ERROR; + case 11: + return INTERNAL_ERROR; + } + + return new SpdySessionStatus(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 SpdySessionStatus(int code, String statusPhrase) { + 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 SpdySessionStatus)) { + return false; + } + + return getCode() == ((SpdySessionStatus) o).getCode(); + } + + @Override + public String toString() { + return getStatusPhrase(); + } + + @Override + public int compareTo(SpdySessionStatus o) { + return getCode() - o.getCode(); + } +} 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..96d237ceb4 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySettingsFrame.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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; + int SETTINGS_CLIENT_CERTIFICATE_VECTOR_SIZE = 8; + + /** + * 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 exceed 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..ddae236725 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java @@ -0,0 +1,188 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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"); + + /** + * 8 Stream In Use + */ + public static final SpdyStreamStatus STREAM_IN_USE = + new SpdyStreamStatus(8, "STREAM_IN_USE"); + + /** + * 9 Stream Already Closed + */ + public static final SpdyStreamStatus STREAM_ALREADY_CLOSED = + new SpdyStreamStatus(9, "STREAM_ALREADY_CLOSED"); + + /** + * 10 Invalid Credentials + */ + public static final SpdyStreamStatus INVALID_CREDENTIALS = + new SpdyStreamStatus(10, "INVALID_CREDENTIALS"); + + /** + * 11 Frame Too Large + */ + public static final SpdyStreamStatus FRAME_TOO_LARGE = + new SpdyStreamStatus(11, "FRAME_TOO_LARGE"); + + /** + * 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; + case 8: + return STREAM_IN_USE; + case 9: + return STREAM_ALREADY_CLOSED; + case 10: + return INVALID_CREDENTIALS; + case 11: + return FRAME_TOO_LARGE; + } + + 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(); + } + + @Override + 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..a30c1565d3 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynReplyFrame.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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..4e2e069a91 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySynStreamFrame.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package 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 7 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/SpdyWindowUpdateFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java new file mode 100644 index 0000000000..4935cf2ada --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyWindowUpdateFrame.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +/** + * A SPDY Protocol WINDOW_UPDATE Control Frame + */ +public interface SpdyWindowUpdateFrame { + + /** + * 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 Delta-Window-Size of this frame. + */ + int getDeltaWindowSize(); + + /** + * Sets the Delta-Window-Size of this frame. + * The Delta-Window-Size must be positive. + */ + void setDeltaWindowSize(int deltaWindowSize); +} 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..a43006d6bc --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +/** + * 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..06be085c03 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/AbstractSocketSpdyEchoTest.java @@ -0,0 +1,291 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + +import static io.netty.handler.codec.spdy.SpdyCodecUtil.*; +import static org.junit.Assert.*; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ChannelBuffer; +import io.netty.buffer.ChannelBuffers; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInboundHandlerContext; +import io.netty.channel.ChannelInboundMessageHandlerAdapter; +import io.netty.channel.ChannelInboundStreamHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.util.SocketAddresses; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +public abstract class AbstractSocketSpdyEchoTest { + + private static final Random random = new Random(); + static final int ignoredBytes = 20; + + private static ChannelBuffer createFrames(int version) { + int length = version < 3 ? 1176 : 1174; + ChannelBuffer frames = ChannelBuffers.buffer(length); + + // SPDY UNKNOWN Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(0xFFFF); + frames.writeByte(0xFF); + frames.writeMedium(4); + frames.writeInt(random.nextInt()); + + // SPDY NOOP Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(5); + frames.writeInt(0); + + // SPDY Data Frame + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + 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(version); + frames.writeShort(1); + frames.writeByte(0x03); + if (version < 3) { + frames.writeMedium(12); + } else { + frames.writeMedium(10); + } + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + frames.writeShort(0x8000); + if (version < 3) { + frames.writeShort(0); + } + + // SPDY SYN_REPLY Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(2); + frames.writeByte(0x01); + if (version < 3) { + frames.writeMedium(8); + } else { + frames.writeMedium(4); + } + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + if (version < 3) { + frames.writeInt(0); + } + + // SPDY RST_STREAM Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(3); + frames.writeInt(8); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + frames.writeInt(random.nextInt() | 0x01); + + // SPDY SETTINGS Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(4); + frames.writeByte(0x01); + frames.writeMedium(12); + frames.writeInt(1); + if (version < 3) { + frames.writeMedium(random.nextInt()); + frames.writeByte(0x03); + } else { + frames.writeByte(0x03); + frames.writeMedium(random.nextInt()); + } + frames.writeInt(random.nextInt()); + + // SPDY PING Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(6); + frames.writeInt(4); + frames.writeInt(random.nextInt()); + + // SPDY GOAWAY Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(7); + if (version < 3) { + frames.writeInt(4); + } else { + frames.writeInt(8); + } + frames.writeInt(random.nextInt() & 0x7FFFFFFF); + if (version >= 3) { + frames.writeInt(random.nextInt() | 0x01); + } + + // SPDY HEADERS Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(8); + frames.writeByte(0x01); + frames.writeMedium(4); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + + // SPDY WINDOW_UPDATE Frame + frames.writeByte(0x80); + frames.writeByte(version); + frames.writeShort(9); + frames.writeInt(8); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + frames.writeInt(random.nextInt() & 0x7FFFFFFF | 0x01); + + return frames; + } + + private ServerBootstrap sb; + private Bootstrap cb; + + protected abstract ServerBootstrap newServerBootstrap(); + protected abstract Bootstrap newClientBootstrap(); + + @Test + public void testSpdyEcho() throws Throwable { + for (int version = SPDY_MIN_VERSION; version <= SPDY_MAX_VERSION; version ++) { + sb = newServerBootstrap(); + cb = newClientBootstrap(); + try { + testSpdyEcho(version); + } finally { + sb.shutdown(); + cb.shutdown(); + } + } + } + + private void testSpdyEcho(final int version) throws Throwable { + + ChannelBuffer frames = createFrames(version); + + final SpdyEchoTestServerHandler sh = new SpdyEchoTestServerHandler(); + final SpdyEchoTestClientHandler ch = new SpdyEchoTestClientHandler(frames); + + sb.childInitializer(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel channel) throws Exception { + channel.pipeline().addLast( + new SpdyFrameDecoder(version), + new SpdyFrameEncoder(version), + sh); + } + }); + + cb.initializer(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel channel) throws Exception { + channel.pipeline().addLast(ch); + } + }); + + Channel sc = sb.localAddress(0).bind().sync().channel(); + int port = ((InetSocketAddress) sc.localAddress()).getPort(); + + Channel cc = cb.remoteAddress(SocketAddresses.LOCALHOST, port).connect().sync().channel(); + 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. + } + } + + 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 SpdyEchoTestServerHandler extends ChannelInboundMessageHandlerAdapter { + final AtomicReference exception = new AtomicReference(); + + @Override + public void messageReceived(ChannelInboundHandlerContext ctx, Object msg) throws Exception { + ctx.write(msg); + } + + @Override + public void exceptionCaught(ChannelInboundHandlerContext ctx, Throwable cause) throws Exception { + if (exception.compareAndSet(null, cause)) { + ctx.close(); + } + } + } + + private class SpdyEchoTestClientHandler extends ChannelInboundStreamHandlerAdapter { + final AtomicReference exception = new AtomicReference(); + final ChannelBuffer frames; + volatile int counter; + + SpdyEchoTestClientHandler(ChannelBuffer frames) { + this.frames = frames; + } + + @Override + public void inboundBufferUpdated(ChannelInboundHandlerContext ctx) throws Exception { + ChannelBuffer m = ctx.inbound().byteBuffer(); + byte[] actual = new byte[m.readableBytes()]; + m.readBytes(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(ChannelInboundHandlerContext ctx, Throwable cause) throws Exception { + if (exception.compareAndSet(null, cause)) { + ctx.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..e127f80129 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/NioNioSocketSpdyEchoTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.socket.nio.NioEventLoop; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; + +public class NioNioSocketSpdyEchoTest extends AbstractSocketSpdyEchoTest { + + @Override + protected Bootstrap newClientBootstrap() { + return new Bootstrap().eventLoop(new NioEventLoop()).channel(new NioSocketChannel()); + } + + @Override + protected ServerBootstrap newServerBootstrap() { + return new ServerBootstrap().eventLoop(new NioEventLoop(), new NioEventLoop()).channel(new NioServerSocketChannel()); + } + +} 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..301bccbcdc --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/NioOioSocketSpdyEchoTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.socket.nio.NioEventLoop; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.socket.oio.OioEventLoop; +import io.netty.channel.socket.oio.OioServerSocketChannel; + +public class NioOioSocketSpdyEchoTest extends AbstractSocketSpdyEchoTest { + + @Override + protected Bootstrap newClientBootstrap() { + return new Bootstrap().eventLoop(new NioEventLoop()).channel(new NioSocketChannel()); + } + + @Override + protected ServerBootstrap newServerBootstrap() { + return new ServerBootstrap().eventLoop(new OioEventLoop(), new OioEventLoop()).channel(new OioServerSocketChannel()); + } +} 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..fd0b5b56d6 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/OioNioSocketSpdyEchoTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.socket.nio.NioEventLoop; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.oio.OioEventLoop; +import io.netty.channel.socket.oio.OioSocketChannel; + +public class OioNioSocketSpdyEchoTest extends AbstractSocketSpdyEchoTest { + + @Override + protected Bootstrap newClientBootstrap() { + return new Bootstrap().eventLoop(new OioEventLoop()).channel(new OioSocketChannel()); + } + + @Override + protected ServerBootstrap newServerBootstrap() { + return new ServerBootstrap().eventLoop(new NioEventLoop(), new NioEventLoop()).channel(new NioServerSocketChannel()); + } +} 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..26e3dedd1d --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/OioOioSocketSpdyEchoTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.spdy; + + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.socket.oio.OioEventLoop; +import io.netty.channel.socket.oio.OioServerSocketChannel; +import io.netty.channel.socket.oio.OioSocketChannel; + +public class OioOioSocketSpdyEchoTest extends AbstractSocketSpdyEchoTest { + + @Override + protected Bootstrap newClientBootstrap() { + return new Bootstrap().eventLoop(new OioEventLoop()).channel(new OioSocketChannel()); + } + + @Override + protected ServerBootstrap newServerBootstrap() { + return new ServerBootstrap().eventLoop(new OioEventLoop(), new OioEventLoop()).channel(new OioServerSocketChannel()); + } +}