diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyDataFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyDataFrame.java new file mode 100644 index 0000000000..6dc6bcbfcf --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyDataFrame.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyDataFrame} implementation. + */ +public class DefaultSpdyDataFrame implements SpdyDataFrame { + + private int streamID; + private boolean last; + private boolean compressed; + private ChannelBuffer data = ChannelBuffers.EMPTY_BUFFER; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + */ + public DefaultSpdyDataFrame(int streamID) { + setStreamID(streamID); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public boolean isLast() { + return last; + } + + public void setLast(boolean last) { + this.last = last; + } + + public boolean isCompressed() { + return compressed; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + + public ChannelBuffer getData() { + return data; + } + + public void setData(ChannelBuffer data) { + if (data == null) { + data = ChannelBuffers.EMPTY_BUFFER; + } + if (data.readableBytes() > SpdyCodecUtil.SPDY_MAX_LENGTH) { + throw new IllegalArgumentException("data payload cannot exceed " + + SpdyCodecUtil.SPDY_MAX_LENGTH + " bytes"); + } + this.data = data; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append("(last: "); + buf.append(isLast()); + buf.append("; compressed: "); + buf.append(isCompressed()); + buf.append(')'); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Size = "); + buf.append(data.readableBytes()); + return buf.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java new file mode 100644 index 0000000000..c86472e24a --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyGoAwayFrame} implementation. + */ +public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame { + + private int lastGoodStreamID; + + /** + * Creates a new instance. + * + * @param lastGoodStreamID the Last-good-stream-ID of this frame + */ + public DefaultSpdyGoAwayFrame(int lastGoodStreamID) { + setLastGoodStreamID(lastGoodStreamID); + } + + public int getLastGoodStreamID() { + return lastGoodStreamID; + } + + public void setLastGoodStreamID(int lastGoodStreamID) { + if (lastGoodStreamID < 0) { + throw new IllegalArgumentException("Last-good-stream-ID" + + " cannot be negative: " + lastGoodStreamID); + } + this.lastGoodStreamID = lastGoodStreamID; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> Last-good-stream-ID = "); + buf.append(lastGoodStreamID); + return buf.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java new file mode 100644 index 0000000000..5f6504db7b --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeaderBlock.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyHeaderBlock} implementation. + */ +public class DefaultSpdyHeaderBlock implements SpdyHeaderBlock { + + private boolean invalid; + private final SpdyHeaders headers = new SpdyHeaders(); + + /** + * Creates a new instance. + */ + protected DefaultSpdyHeaderBlock() { + } + + public boolean isInvalid() { + return invalid; + } + + public void setInvalid() { + this.invalid = true; + } + + public void addHeader(final String name, final Object value) { + headers.addHeader(name, value); + } + + public void setHeader(final String name, final Object value) { + headers.setHeader(name, value); + } + + public void setHeader(final String name, final Iterable values) { + headers.setHeader(name, values); + } + + public void removeHeader(final String name) { + headers.removeHeader(name); + } + + public void clearHeaders() { + headers.clearHeaders(); + } + + public String getHeader(final String name) { + List values = getHeaders(name); + return values.size() > 0 ? values.get(0) : null; + } + + public List getHeaders(final String name) { + return headers.getHeaders(name); + } + + public List> getHeaders() { + return headers.getHeaders(); + } + + public boolean containsHeader(final String name) { + return headers.containsHeader(name); + } + + public Set getHeaderNames() { + return headers.getHeaderNames(); + } + + protected void appendHeaders(StringBuilder buf) { + for (Map.Entry e: getHeaders()) { + buf.append(" "); + buf.append(e.getKey()); + buf.append(": "); + buf.append(e.getValue()); + buf.append(StringUtil.NEWLINE); + } + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java new file mode 100644 index 0000000000..769e31a748 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyHeadersFrame.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyHeadersFrame} implementation. + */ +public class DefaultSpdyHeadersFrame extends DefaultSpdyHeaderBlock + implements SpdyHeadersFrame { + + private int streamID; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + */ + public DefaultSpdyHeadersFrame(int streamID) { + super(); + setStreamID(streamID); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Headers:"); + buf.append(StringUtil.NEWLINE); + appendHeaders(buf); + + // Remove the last newline. + buf.setLength(buf.length() - StringUtil.NEWLINE.length()); + return buf.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java new file mode 100644 index 0000000000..03196f7ffd --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyNoOpFrame.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +/** + * The default {@link SpdyNoOpFrame} implementation. + */ +public class DefaultSpdyNoOpFrame implements SpdyNoOpFrame { + + /** + * Creates a new instance. + */ + public DefaultSpdyNoOpFrame() { + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + return buf.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyPingFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyPingFrame.java new file mode 100644 index 0000000000..3277132097 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyPingFrame.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyPingFrame} implementation. + */ +public class DefaultSpdyPingFrame implements SpdyPingFrame { + + private int ID; + + /** + * Creates a new instance. + * + * @param ID the unique ID of this frame + */ + public DefaultSpdyPingFrame(int ID) { + setID(ID); + } + + public int getID() { + return ID; + } + + public void setID(int ID) { + this.ID = ID; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> ID = "); + buf.append(ID); + return buf.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyRstStreamFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyRstStreamFrame.java new file mode 100644 index 0000000000..a6bef20ed3 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdyRstStreamFrame.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdyRstStreamFrame} implementation. + */ +public class DefaultSpdyRstStreamFrame implements SpdyRstStreamFrame { + + private int streamID; + private SpdyStreamStatus status; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + * @param statusCode the Status code of this frame + */ + public DefaultSpdyRstStreamFrame(int streamID, int statusCode) { + this(streamID, SpdyStreamStatus.valueOf(statusCode)); + } + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + * @param status the status of this frame + */ + public DefaultSpdyRstStreamFrame(int streamID, SpdyStreamStatus status) { + setStreamID(streamID); + setStatus(status); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public SpdyStreamStatus getStatus() { + return status; + } + + public void setStatus(SpdyStreamStatus status) { + this.status = status; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Status: "); + buf.append(status.toString()); + return buf.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySettingsFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySettingsFrame.java new file mode 100644 index 0000000000..71f254903b --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySettingsFrame.java @@ -0,0 +1,188 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdySettingsFrame} implementation. + */ +public class DefaultSpdySettingsFrame implements SpdySettingsFrame { + + private boolean clear; + private final Map settingsMap = new TreeMap(); + + /** + * Creates a new instance. + */ + public DefaultSpdySettingsFrame() { + } + + public Set getIDs() { + return settingsMap.keySet(); + } + + public boolean isSet(int ID) { + Integer key = new Integer(ID); + return settingsMap.containsKey(key); + } + + public int getValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getValue(); + } else { + return -1; + } + } + + public void setValue(int ID, int value) { + setValue(ID, value, false, false); + } + + public void setValue(int ID, int value, boolean persistValue, boolean persisted) { + if (ID <= 0 || ID > SpdyCodecUtil.SPDY_SETTINGS_MAX_ID) { + throw new IllegalArgumentException("Setting ID is not valid: " + ID); + } + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + Setting setting = settingsMap.get(key); + setting.setValue(value); + setting.setPersist(persistValue); + setting.setPersisted(persisted); + } else { + settingsMap.put(key, new Setting(value, persistValue, persisted)); + } + } + + public void removeValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.remove(key); + } + } + + public boolean persistValue(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getPersist(); + } else { + return false; + } + } + + public void setPersistValue(int ID, boolean persistValue) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.get(key).setPersist(persistValue); + } + } + + public boolean isPersisted(int ID) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + return settingsMap.get(key).getPersisted(); + } else { + return false; + } + } + + public void setPersisted(int ID, boolean persisted) { + Integer key = new Integer(ID); + if (settingsMap.containsKey(key)) { + settingsMap.get(key).setPersisted(persisted); + } + } + + public boolean clearPreviouslyPersistedSettings() { + return clear; + } + + public void setClearPreviouslyPersistedSettings(boolean clear) { + this.clear = clear; + } + + private Set> getSettings() { + return settingsMap.entrySet(); + } + + private void appendSettings(StringBuilder buf) { + for (Map.Entry e: getSettings()) { + Setting setting = e.getValue(); + buf.append("--> "); + buf.append(e.getKey().toString()); + buf.append(":"); + buf.append(setting.getValue()); + buf.append(" (persist value: "); + buf.append(setting.getPersist()); + buf.append("; persisted: "); + buf.append(setting.getPersisted()); + buf.append(')'); + buf.append(StringUtil.NEWLINE); + } + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append(StringUtil.NEWLINE); + appendSettings(buf); + buf.setLength(buf.length() - StringUtil.NEWLINE.length()); + return buf.toString(); + } + + private static final class Setting { + + private int value; + private boolean persist; + private boolean persisted; + + public Setting(int value, boolean persist, boolean persisted) { + this.value = value; + this.persist = persist; + this.persisted = persisted; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public boolean getPersist() { + return persist; + } + + public void setPersist(boolean persist) { + this.persist = persist; + } + + public boolean getPersisted() { + return persisted; + } + + public void setPersisted(boolean persisted) { + this.persisted = persisted; + } + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java new file mode 100644 index 0000000000..f4792560ba --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdySynReplyFrame} implementation. + */ +public class DefaultSpdySynReplyFrame extends DefaultSpdyHeaderBlock + implements SpdySynReplyFrame { + + private int streamID; + private boolean last; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + */ + public DefaultSpdySynReplyFrame(int streamID) { + super(); + setStreamID(streamID); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public boolean isLast() { + return last; + } + + public void setLast(boolean last) { + this.last = last; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append("(last: "); + buf.append(isLast()); + buf.append(')'); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + buf.append("--> Headers:"); + buf.append(StringUtil.NEWLINE); + appendHeaders(buf); + + // Remove the last newline. + buf.setLength(buf.length() - StringUtil.NEWLINE.length()); + return buf.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java new file mode 100644 index 0000000000..c9c24d946c --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import org.jboss.netty.util.internal.StringUtil; + +/** + * The default {@link SpdySynStreamFrame} implementation. + */ +public class DefaultSpdySynStreamFrame extends DefaultSpdyHeaderBlock + implements SpdySynStreamFrame { + + private int streamID; + private int associatedToStreamID; + private byte priority; + private boolean last; + private boolean unidirectional; + + /** + * Creates a new instance. + * + * @param streamID the Stream-ID of this frame + * @param associatedToStreamID the Associated-To-Stream-ID of this frame + * @param priority the priority of the stream + */ + public DefaultSpdySynStreamFrame( + int streamID, int associatedToStreamID, byte priority) { + super(); + setStreamID(streamID); + setAssociatedToStreamID(associatedToStreamID); + setPriority(priority); + } + + public int getStreamID() { + return streamID; + } + + public void setStreamID(int streamID) { + if (streamID <= 0) { + throw new IllegalArgumentException( + "Stream-ID must be positive: " + streamID); + } + this.streamID = streamID; + } + + public int getAssociatedToStreamID() { + return associatedToStreamID; + } + + public void setAssociatedToStreamID(int associatedToStreamID) { + if (associatedToStreamID < 0) { + throw new IllegalArgumentException( + "Associated-To-Stream-ID cannot be negative: " + + associatedToStreamID); + } + this.associatedToStreamID = associatedToStreamID; + } + + public byte getPriority() { + return priority; + } + + public void setPriority(byte priority) { + if (priority < 0 || priority > 3) { + throw new IllegalArgumentException( + "Priortiy must be between 0 and 3 inclusive: " + priority); + } + this.priority = priority; + } + + public boolean isLast() { + return last; + } + + public void setLast(boolean last) { + this.last = last; + } + + public boolean isUnidirectional() { + return unidirectional; + } + + public void setUnidirectional(boolean unidirectional) { + this.unidirectional = unidirectional; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append("(last: "); + buf.append(isLast()); + buf.append("; unidirectional: "); + buf.append(isUnidirectional()); + buf.append(')'); + buf.append(StringUtil.NEWLINE); + buf.append("--> Stream-ID = "); + buf.append(streamID); + buf.append(StringUtil.NEWLINE); + if (associatedToStreamID != 0) { + buf.append("--> Associated-To-Stream-ID = "); + buf.append(associatedToStreamID); + buf.append(StringUtil.NEWLINE); + } + buf.append("--> Priority = "); + buf.append(priority); + buf.append(StringUtil.NEWLINE); + buf.append("--> Headers:"); + buf.append(StringUtil.NEWLINE); + appendHeaders(buf); + + // Remove the last newline. + buf.setLength(buf.length() - StringUtil.NEWLINE.length()); + return buf.toString(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyCodecUtil.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyCodecUtil.java new file mode 100644 index 0000000000..9a88d60c41 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyCodecUtil.java @@ -0,0 +1,178 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.util.Formatter; + +import org.jboss.netty.buffer.ChannelBuffer; + +final class SpdyCodecUtil { + + static final int SPDY_VERSION = 2; + + static final int SPDY_HEADER_TYPE_OFFSET = 2; + static final int SPDY_HEADER_FLAGS_OFFSET = 4; + static final int SPDY_HEADER_LENGTH_OFFSET = 5; + static final int SPDY_HEADER_SIZE = 8; + + static final int SPDY_MAX_LENGTH = 0xFFFFFF; // Length is a 24-bit field + + static final byte SPDY_DATA_FLAG_FIN = 0x01; + static final byte SPDY_DATA_FLAG_COMPRESS = 0x02; + + static final int SPDY_SYN_STREAM_FRAME = 1; + static final int SPDY_SYN_REPLY_FRAME = 2; + static final int SPDY_RST_STREAM_FRAME = 3; + static final int SPDY_SETTINGS_FRAME = 4; + static final int SPDY_NOOP_FRAME = 5; + static final int SPDY_PING_FRAME = 6; + static final int SPDY_GOAWAY_FRAME = 7; + static final int SPDY_HEADERS_FRAME = 8; + static final int SPDY_WINDOW_UPDATE_FRAME = 9; + + static final byte SPDY_FLAG_FIN = 0x01; + static final byte SPDY_FLAG_UNIDIRECTIONAL = 0x02; + + static final byte SPDY_SETTINGS_CLEAR = 0x01; + static final byte SPDY_SETTINGS_PERSIST_VALUE = 0x01; + static final byte SPDY_SETTINGS_PERSISTED = 0x02; + + static final int SPDY_SETTINGS_MAX_ID = 0xFFFFFF; // ID is a 24-bit field + + static final int SPDY_MAX_NV_LENGTH = 0xFFFF; // Length is a 16-bit field + + // Zlib Dictionary + private static final String SPDY_DICT_S = + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" + + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" + + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" + + "-agent10010120020120220320420520630030130230330430530630740040140240340440" + + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" + + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" + + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" + + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" + + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" + + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" + + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" + + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" + + ".1statusversionurl "; + static final byte[] SPDY_DICT; + static { + byte[] SPDY_DICT_ = null; + + try { + SPDY_DICT_ = SPDY_DICT_S.getBytes("US-ASCII"); + // dictionary is null terminated + SPDY_DICT_[SPDY_DICT_.length - 1] = (byte) 0; + } catch (Exception e) { + SPDY_DICT_ = new byte[1]; + } + + SPDY_DICT = SPDY_DICT_; + } + + + private SpdyCodecUtil() { + } + + + /** + * Reads a big-endian unsigned short integer from the buffer. + */ + static int getUnsignedShort(ChannelBuffer buf, int offset) { + return (int) ((buf.getByte(offset) & 0xFF) << 8 | + (buf.getByte(offset + 1) & 0xFF)); + } + + /** + * Reads a big-endian unsigned medium integer from the buffer. + */ + static int getUnsignedMedium(ChannelBuffer buf, int offset) { + return (int) ((buf.getByte(offset) & 0xFF) << 16 | + (buf.getByte(offset + 1) & 0xFF) << 8 | + (buf.getByte(offset + 2) & 0xFF)); + } + + /** + * Reads a big-endian (31-bit) integer from the buffer. + */ + static int getUnsignedInt(ChannelBuffer buf, int offset) { + return (int) ((buf.getByte(offset) & 0x7F) << 24 | + (buf.getByte(offset + 1) & 0xFF) << 16 | + (buf.getByte(offset + 2) & 0xFF) << 8 | + (buf.getByte(offset + 3) & 0xFF)); + } + + /** + * Reads a big-endian signed integer from the buffer. + */ + static int getSignedInt(ChannelBuffer buf, int offset) { + return (int) ((buf.getByte(offset) & 0xFF) << 24 | + (buf.getByte(offset + 1) & 0xFF) << 16 | + (buf.getByte(offset + 2) & 0xFF) << 8 | + (buf.getByte(offset + 3) & 0xFF)); + } + + /** + * Validate a SPDY header name. + */ + static void validateHeaderName(String name) { + if (name == null) { + throw new NullPointerException("name"); + } + if (name.length() == 0) { + throw new IllegalArgumentException( + "name cannot be length zero"); + } + // Since name may only contain ascii characters, for valid names + // name.length() returns the number of bytes when UTF-8 encoded. + if (name.length() > SPDY_MAX_NV_LENGTH) { + throw new IllegalArgumentException( + "name exceeds allowable length: " + name); + } + for (int i = 0; i < name.length(); i ++) { + char c = name.charAt(i); + if (c == 0) { + throw new IllegalArgumentException( + "name contains null character: " + name); + } + if (c > 127) { + throw new IllegalArgumentException( + "name contains non-ascii character: " + name); + } + } + } + + /** + * Validate a SPDY header value. Does not validate max length. + */ + static void validateHeaderValue(String value) { + if (value == null) { + throw new NullPointerException("value"); + } + if (value.length() == 0) { + throw new IllegalArgumentException( + "value cannot be length zero"); + } + for (int i = 0; i < value.length(); i ++) { + char c = value.charAt(i); + if (c == 0) { + throw new IllegalArgumentException( + "value contains null character: " + value); + } + } + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyDataFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyDataFrame.java new file mode 100644 index 0000000000..c67c6aa252 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyDataFrame.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * A SPDY Protocol Data Frame + */ +public interface SpdyDataFrame { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); + + /** + * Returns {@code true} if this frame is the last frame to be transmitted + * on the stream. + */ + boolean isLast(); + + /** + * Sets if this frame is the last frame to be transmitted on the stream. + */ + void setLast(boolean last); + + /** + * Returns {@code true} if the data in this frame has been compressed. + */ + boolean isCompressed(); + + /** + * Sets if the data in this frame has been compressed. + */ + void setCompressed(boolean compressed); + + /** + * Returns the data payload of this frame. If there is no data payload + * {@link ChannelBuffers#EMPTY_BUFFER} is returned. + */ + ChannelBuffer getData(); + + /** + * Sets the data payload of this frame. If {@code null} is specified, + * the data payload will be set to {@link ChannelBuffers#EMPTY_BUFFER}. + * The data payload cannot exceed 16777215 bytes. + */ + void setData(ChannelBuffer data); +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameCodec.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameCodec.java new file mode 100644 index 0000000000..5261295081 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameCodec.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import org.jboss.netty.channel.ChannelDownstreamHandler; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelUpstreamHandler; + +/** + * A combination of {@link SpdyFrameDecoder} and {@link SpdyFrameEncoder}. + * @apiviz.has org.jboss.netty.handler.codec.spdy.SpdyFrameDecoder + * @apiviz.has org.jboss.netty.handler.codec.spdy.SpdyFrameEncoder + */ +public class SpdyFrameCodec implements ChannelUpstreamHandler, + ChannelDownstreamHandler { + + private final SpdyFrameDecoder decoder = new SpdyFrameDecoder(); + private final SpdyFrameEncoder encoder = new SpdyFrameEncoder(); + + public SpdyFrameCodec() { + } + + public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) + throws Exception { + decoder.handleUpstream(ctx, e); + } + + public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e) + throws Exception { + encoder.handleDownstream(ctx, e); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoder.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoder.java new file mode 100644 index 0000000000..fc9bddf990 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameDecoder.java @@ -0,0 +1,321 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.util.zip.Inflater; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.compression.ZlibDecoder; +import org.jboss.netty.handler.codec.embedder.DecoderEmbedder; +import org.jboss.netty.handler.codec.frame.FrameDecoder; + +import static org.jboss.netty.handler.codec.spdy.SpdyCodecUtil.*; + +/** + * Decodes {@link ChannelBuffer}s into SPDY Data and Control Frames. + */ +public class SpdyFrameDecoder extends FrameDecoder { + + private final DecoderEmbedder headerBlockDecompressor = + new DecoderEmbedder(new ZlibDecoder(SPDY_DICT)); + + public SpdyFrameDecoder() { + super(); + } + + @Override + protected Object decode( + ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) + throws Exception { + + // Must read common header to determine frame length + if (buffer.readableBytes() < SPDY_HEADER_SIZE) { + return null; + } + + // Get frame length from common header + int frameOffset = buffer.readerIndex(); + int lengthOffset = frameOffset + SPDY_HEADER_LENGTH_OFFSET; + int dataLength = getUnsignedMedium(buffer, lengthOffset); + int frameLength = SPDY_HEADER_SIZE + dataLength; + + // Wait until entire frame is readable + if (buffer.readableBytes() < frameLength) { + return null; + } + + // Read common header fields + boolean control = (buffer.getByte(frameOffset) & 0x80) != 0; + int flagsOffset = frameOffset + SPDY_HEADER_FLAGS_OFFSET; + byte flags = buffer.getByte(flagsOffset); + + if (control) { + // Decode control frame common header + int version = getUnsignedShort(buffer, frameOffset) & 0x7FFF; + + // Spdy versioning spec is broken + if (version != SPDY_VERSION) { + buffer.skipBytes(frameLength); + throw new SpdyProtocolException( + "Unsupported version: " + version); + } + + int typeOffset = frameOffset + SPDY_HEADER_TYPE_OFFSET; + int type = getUnsignedShort(buffer, typeOffset); + buffer.skipBytes(SPDY_HEADER_SIZE); + + return decodeControlFrame(type, flags, buffer.readBytes(dataLength)); + } else { + // Decode data frame common header + int streamID = getUnsignedInt(buffer, frameOffset); + buffer.skipBytes(SPDY_HEADER_SIZE); + + SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(streamID); + spdyDataFrame.setLast((flags & SPDY_DATA_FLAG_FIN) != 0); + spdyDataFrame.setCompressed((flags & SPDY_DATA_FLAG_COMPRESS) != 0); + spdyDataFrame.setData(buffer.readBytes(dataLength)); + + return spdyDataFrame; + } + } + + private Object decodeControlFrame(int type, byte flags, ChannelBuffer data) + throws Exception { + int streamID; + boolean last; + + switch (type) { + case SPDY_SYN_STREAM_FRAME: + if (data.readableBytes() < 12) { + throw new SpdyProtocolException( + "Received invalid SYN_STREAM control frame"); + } + streamID = getUnsignedInt(data, data.readerIndex()); + int associatedToStreamID = getUnsignedInt(data, data.readerIndex() + 4); + byte priority = (byte) (data.getByte(data.readerIndex() + 8) >> 6 & 0x03); + data.skipBytes(10); + + SpdySynStreamFrame spdySynStreamFrame = + new DefaultSpdySynStreamFrame(streamID, associatedToStreamID, priority); + + last = (flags & SPDY_FLAG_FIN) != 0; + boolean unid = (flags & SPDY_FLAG_UNIDIRECTIONAL) != 0; + spdySynStreamFrame.setLast(last); + spdySynStreamFrame.setUnidirectional(unid); + + decodeHeaderBlock(spdySynStreamFrame, decompress(data)); + + return spdySynStreamFrame; + + case SPDY_SYN_REPLY_FRAME: + if (data.readableBytes() < 8) { + throw new SpdyProtocolException( + "Received invalid SYN_REPLY control frame"); + } + streamID = getUnsignedInt(data, data.readerIndex()); + data.skipBytes(6); + + SpdySynReplyFrame spdySynReplyFrame = + new DefaultSpdySynReplyFrame(streamID); + + last = (flags & SPDY_FLAG_FIN) != 0; + spdySynReplyFrame.setLast(last); + + decodeHeaderBlock(spdySynReplyFrame, decompress(data)); + + return spdySynReplyFrame; + + case SPDY_RST_STREAM_FRAME: + if (flags != 0 || data.readableBytes() != 8) { + throw new SpdyProtocolException( + "Received invalid RST_STREAM control frame"); + } + streamID = getUnsignedInt(data, data.readerIndex()); + int statusCode = getSignedInt(data, data.readerIndex() + 4); + if (statusCode == 0) { + throw new SpdyProtocolException( + "Received invalid RST_STREAM status code"); + } + + return new DefaultSpdyRstStreamFrame(streamID, statusCode); + + case SPDY_SETTINGS_FRAME: + if (data.readableBytes() < 4) { + throw new SpdyProtocolException( + "Received invalid SETTINGS control frame"); + } + // Each ID/Value entry is 8 bytes + // The number of entries cannot exceed SPDY_MAX_LENGTH / 8; + int numEntries = getUnsignedInt(data, data.readerIndex()); + if ((numEntries > (SPDY_MAX_LENGTH - 4) / 8) || + (data.readableBytes() != numEntries * 8 + 4)) { + throw new SpdyProtocolException( + "Received invalid SETTINGS control frame"); + } + data.skipBytes(4); + + SpdySettingsFrame spdySettingsFrame = new DefaultSpdySettingsFrame(); + + boolean clear = (flags & SPDY_SETTINGS_CLEAR) != 0; + spdySettingsFrame.setClearPreviouslyPersistedSettings(clear); + + for (int i = 0; i < numEntries; i ++) { + // Chromium Issue 79156 + // SPDY setting ids are not written in network byte order + // Read id assuming the architecture is little endian + int ID = (data.readByte() & 0xFF) | + (data.readByte() & 0xFF) << 8 | + (data.readByte() & 0xFF) << 16; + byte ID_flags = data.readByte(); + int value = getSignedInt(data, data.readerIndex()); + data.skipBytes(4); + + if (!(spdySettingsFrame.isSet(ID))) { + boolean persistVal = (ID_flags & SPDY_SETTINGS_PERSIST_VALUE) != 0; + boolean persisted = (ID_flags & SPDY_SETTINGS_PERSISTED) != 0; + spdySettingsFrame.setValue(ID, value, persistVal, persisted); + } + } + + return spdySettingsFrame; + + case SPDY_NOOP_FRAME: + if (data.readableBytes() != 0) { + throw new SpdyProtocolException( + "Received invalid NOOP control frame"); + } + + return null; + + case SPDY_PING_FRAME: + if (data.readableBytes() != 4) { + throw new SpdyProtocolException( + "Received invalid PING control frame"); + } + int ID = getSignedInt(data, data.readerIndex()); + + return new DefaultSpdyPingFrame(ID); + + case SPDY_GOAWAY_FRAME: + if (data.readableBytes() != 4) { + throw new SpdyProtocolException( + "Received invalid GOAWAY control frame"); + } + int lastGoodStreamID = getUnsignedInt(data, data.readerIndex()); + + return new DefaultSpdyGoAwayFrame(lastGoodStreamID); + + case SPDY_HEADERS_FRAME: + // Protocol allows length 4 frame when there are no name/value pairs + if (data.readableBytes() == 4) { + streamID = getUnsignedInt(data, data.readerIndex()); + return new DefaultSpdyHeadersFrame(streamID); + } + + if (data.readableBytes() < 8) { + throw new SpdyProtocolException( + "Received invalid HEADERS control frame"); + } + streamID = getUnsignedInt(data, data.readerIndex()); + data.skipBytes(6); + + SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(streamID); + + decodeHeaderBlock(spdyHeadersFrame, decompress(data)); + + return spdyHeadersFrame; + + case SPDY_WINDOW_UPDATE_FRAME: + return null; + + default: + return null; + } + } + + private ChannelBuffer decompress(ChannelBuffer compressed) throws Exception { + headerBlockDecompressor.offer(compressed); + return headerBlockDecompressor.poll(); + } + + private void decodeHeaderBlock(SpdyHeaderBlock headerFrame, ChannelBuffer headerBlock) + throws Exception { + if (headerBlock.readableBytes() < 2) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + int numEntries = getUnsignedShort(headerBlock, headerBlock.readerIndex()); + headerBlock.skipBytes(2); + for (int i = 0; i < numEntries; i ++) { + if (headerBlock.readableBytes() < 2) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + int nameLength = getUnsignedShort(headerBlock, headerBlock.readerIndex()); + headerBlock.skipBytes(2); + if (nameLength == 0) { + headerFrame.setInvalid(); + return; + } + if (headerBlock.readableBytes() < nameLength) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + byte[] nameBytes = new byte[nameLength]; + headerBlock.readBytes(nameBytes); + String name = new String(nameBytes, "UTF-8"); + if (headerFrame.containsHeader(name)) { + throw new SpdyProtocolException( + "Received duplicate header name: " + name); + } + if (headerBlock.readableBytes() < 2) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + int valueLength = getUnsignedShort(headerBlock, headerBlock.readerIndex()); + headerBlock.skipBytes(2); + if (valueLength == 0) { + headerFrame.setInvalid(); + return; + } + if (headerBlock.readableBytes() < valueLength) { + throw new SpdyProtocolException( + "Received invalid header block"); + } + byte[] valueBytes = new byte[valueLength]; + headerBlock.readBytes(valueBytes); + int index = 0; + int offset = 0; + while (index < valueBytes.length) { + while (index < valueBytes.length && valueBytes[index] != (byte) 0) { + index ++; + } + if (index < valueBytes.length && valueBytes[index + 1] == (byte) 0) { + // Received multiple, in-sequence NULL characters + headerFrame.setInvalid(); + return; + } + String value = new String(valueBytes, offset, index - offset, "UTF-8"); + headerFrame.addHeader(name, value); + index ++; + offset = index; + } + } + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameEncoder.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameEncoder.java new file mode 100644 index 0000000000..98b26b0fc9 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyFrameEncoder.java @@ -0,0 +1,257 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.nio.ByteOrder; +import java.util.Set; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.compression.ZlibEncoder; +import org.jboss.netty.handler.codec.embedder.EncoderEmbedder; +import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; + +import static org.jboss.netty.handler.codec.spdy.SpdyCodecUtil.*; + +/** + * Encodes a SPDY Data or Control Frame into a {@link ChannelBuffer}. + */ +public class SpdyFrameEncoder extends OneToOneEncoder { + + private final EncoderEmbedder headerBlockCompressor = + new EncoderEmbedder(new ZlibEncoder(9, SPDY_DICT)); + + public SpdyFrameEncoder() { + super(); + } + + @Override + protected Object encode( + ChannelHandlerContext ctx, Channel channel, Object msg) + throws Exception { + + if (msg instanceof SpdyDataFrame) { + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + ChannelBuffer data = spdyDataFrame.getData(); + byte flags = spdyDataFrame.isLast() ? SPDY_DATA_FLAG_FIN : 0; + if (spdyDataFrame.isCompressed()) { + flags |= SPDY_DATA_FLAG_COMPRESS; + } + ChannelBuffer header = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE); + header.writeInt(spdyDataFrame.getStreamID() & 0x7FFFFFFF); + header.writeByte(flags); + header.writeMedium(data.readableBytes()); + return ChannelBuffers.wrappedBuffer(header, data); + + } else if (msg instanceof SpdySynStreamFrame) { + + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(spdySynStreamFrame)); + byte flags = spdySynStreamFrame.isLast() ? SPDY_FLAG_FIN : 0; + if (spdySynStreamFrame.isUnidirectional()) { + flags |= SPDY_FLAG_UNIDIRECTIONAL; + } + int headerBlockLength = data.readableBytes(); + int length = (headerBlockLength == 0) ? 12 : 10 + headerBlockLength; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_SYN_STREAM_FRAME); + frame.writeByte(flags); + frame.writeMedium(length); + frame.writeInt(spdySynStreamFrame.getStreamID()); + frame.writeInt(spdySynStreamFrame.getAssociatedToStreamID()); + frame.writeShort(((short) spdySynStreamFrame.getPriority()) << 14); + if (data.readableBytes() == 0) { + frame.writeShort(0); + } + return ChannelBuffers.wrappedBuffer(frame, data); + + } else if (msg instanceof SpdySynReplyFrame) { + + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(spdySynReplyFrame)); + byte flags = spdySynReplyFrame.isLast() ? SPDY_FLAG_FIN : 0; + int headerBlockLength = data.readableBytes(); + int length = (headerBlockLength == 0) ? 8 : 6 + headerBlockLength; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_SYN_REPLY_FRAME); + frame.writeByte(flags); + frame.writeMedium(length); + frame.writeInt(spdySynReplyFrame.getStreamID()); + if (data.readableBytes() == 0) { + frame.writeInt(0); + } else { + frame.writeShort(0); + } + return ChannelBuffers.wrappedBuffer(frame, data); + + } else if (msg instanceof SpdyRstStreamFrame) { + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 8); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_RST_STREAM_FRAME); + frame.writeInt(8); + frame.writeInt(spdyRstStreamFrame.getStreamID()); + frame.writeInt(spdyRstStreamFrame.getStatus().getCode()); + return frame; + + } else if (msg instanceof SpdySettingsFrame) { + + SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; + byte flags = spdySettingsFrame.clearPreviouslyPersistedSettings() ? + SPDY_SETTINGS_CLEAR : 0; + Set IDs = spdySettingsFrame.getIDs(); + int numEntries = IDs.size(); + int length = 4 + numEntries * 8; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_SETTINGS_FRAME); + frame.writeByte(flags); + frame.writeMedium(length); + frame.writeInt(numEntries); + for (Integer ID: IDs) { + int id = ID.intValue(); + byte ID_flags = (byte) 0; + if (spdySettingsFrame.persistValue(id)) { + ID_flags |= SPDY_SETTINGS_PERSIST_VALUE; + } + if (spdySettingsFrame.isPersisted(id)) { + ID_flags |= SPDY_SETTINGS_PERSISTED; + } + // Chromium Issue 79156 + // SPDY setting ids are not written in network byte order + // Write id assuming the architecture is little endian + frame.writeByte((id >> 0) & 0xFF); + frame.writeByte((id >> 8) & 0xFF); + frame.writeByte((id >> 16) & 0xFF); + frame.writeByte(ID_flags); + frame.writeInt(spdySettingsFrame.getValue(id)); + } + return frame; + + } else if (msg instanceof SpdyNoOpFrame) { + + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_NOOP_FRAME); + frame.writeInt(0); + return frame; + + } else if (msg instanceof SpdyPingFrame) { + + SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 4); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_PING_FRAME); + frame.writeInt(4); + frame.writeInt(spdyPingFrame.getID()); + return frame; + + } else if (msg instanceof SpdyGoAwayFrame) { + + SpdyGoAwayFrame spdyGoAwayFrame = (SpdyGoAwayFrame) msg; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + 4); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_GOAWAY_FRAME); + frame.writeInt(4); + frame.writeInt(spdyGoAwayFrame.getLastGoodStreamID()); + return frame; + + } else if (msg instanceof SpdyHeadersFrame) { + + SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; + ChannelBuffer data = compressHeaderBlock( + encodeHeaderBlock(spdyHeadersFrame)); + int headerBlockLength = data.readableBytes(); + int length = (headerBlockLength == 0) ? 4 : 6 + headerBlockLength; + ChannelBuffer frame = ChannelBuffers.buffer( + ByteOrder.BIG_ENDIAN, SPDY_HEADER_SIZE + length); + frame.writeShort(SPDY_VERSION | 0x8000); + frame.writeShort(SPDY_HEADERS_FRAME); + frame.writeInt(length); + frame.writeInt(spdyHeadersFrame.getStreamID()); + if (data.readableBytes() != 0) { + frame.writeShort(0); + } + return ChannelBuffers.wrappedBuffer(frame, data); + } + + // Unknown message type + return msg; + } + + private ChannelBuffer encodeHeaderBlock(SpdyHeaderBlock headerFrame) + throws Exception { + Set names = headerFrame.getHeaderNames(); + int numHeaders = names.size(); + if (numHeaders == 0) { + return ChannelBuffers.EMPTY_BUFFER; + } + if (numHeaders > SPDY_MAX_NV_LENGTH) { + throw new IllegalArgumentException( + "header block contains too many headers"); + } + ChannelBuffer headerBlock = ChannelBuffers.dynamicBuffer( + ByteOrder.BIG_ENDIAN, 256); + headerBlock.writeShort(numHeaders); + for (String name: names) { + byte[] nameBytes = name.getBytes("UTF-8"); + headerBlock.writeShort(nameBytes.length); + headerBlock.writeBytes(nameBytes); + int savedIndex = headerBlock.writerIndex(); + int valueLength = 0; + headerBlock.writeShort(valueLength); + for (String value: headerFrame.getHeaders(name)) { + byte[] valueBytes = value.getBytes("UTF-8"); + headerBlock.writeBytes(valueBytes); + headerBlock.writeByte(0); + valueLength += valueBytes.length + 1; + } + valueLength --; + if (valueLength > SPDY_MAX_NV_LENGTH) { + throw new IllegalArgumentException( + "header exceeds allowable length: " + name); + } + headerBlock.setShort(savedIndex, valueLength); + headerBlock.writerIndex(headerBlock.writerIndex() - 1); + } + return headerBlock; + } + + private synchronized ChannelBuffer compressHeaderBlock( + ChannelBuffer uncompressed) throws Exception { + if (uncompressed.readableBytes() == 0) { + return ChannelBuffers.EMPTY_BUFFER; + } + headerBlockCompressor.offer(uncompressed); + return headerBlockCompressor.poll(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyGoAwayFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyGoAwayFrame.java new file mode 100644 index 0000000000..c565d7a15d --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyGoAwayFrame.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +/** + * A SPDY Protocol GOAWAY Control Frame + */ +public interface SpdyGoAwayFrame { + + /** + * Returns the Last-good-stream-ID of this frame. + */ + int getLastGoodStreamID(); + + /** + * Sets the Last-good-stream-ID of this frame. The Last-good-stream-ID + * cannot be negative. + */ + void setLastGoodStreamID(int lastGoodStreamID); +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaderBlock.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaderBlock.java new file mode 100644 index 0000000000..b1293244ff --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaderBlock.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.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/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaders.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaders.java new file mode 100644 index 0000000000..45627d25dc --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeaders.java @@ -0,0 +1,530 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; + +/** + * Provides the constants for the standard SPDY HTTP header names and commonly + * used utility methods that access an {@link SpdyHeaderBlock}. + * @apiviz.sterotype static + */ +public class SpdyHeaders { + + /** + * SPDY HTTP header names + * @apiviz.sterotype static + */ + public static final class HttpNames { + /** + * {@code "method"} + */ + public static final String METHOD = "method"; + /** + * {@code "scheme"} + */ + public static final String SCHEME = "scheme"; + /** + * {@code "status"} + */ + public static final String STATUS = "status"; + /** + * {@code "url"} + */ + public static final String URL = "url"; + /** + * {@code "version"} + */ + public static final String VERSION = "version"; + + private HttpNames() { + super(); + } + } + + + /** + * Returns the header value with the specified header name. If there are + * more than one header value for the specified header name, the first + * value is returned. + * + * @return the header value or {@code null} if there is no such header + */ + public static String getHeader(SpdyHeaderBlock block, String name) { + return block.getHeader(name); + } + + /** + * Returns the header value with the specified header name. If there are + * more than one header value for the specified header name, the first + * value is returned. + * + * @return the header value or the {@code defaultValue} if there is no such + * header + */ + public static String getHeader(SpdyHeaderBlock block, String name, String defaultValue) { + String value = block.getHeader(name); + if (value == null) { + return defaultValue; + } + return value; + } + + /** + * Sets a new header with the specified name and value. If there is an + * existing header with the same name, the existing header is removed. + */ + public static void setHeader(SpdyHeaderBlock block, String name, Object value) { + block.setHeader(name, value); + } + + /** + * Sets a new header with the specified name and values. If there is an + * existing header with the same name, the existing header is removed. + */ + public static void setHeader(SpdyHeaderBlock block, String name, Iterable values) { + block.setHeader(name, values); + } + + /** + * Adds a new header with the specified name and value. + */ + public static void addHeader(SpdyHeaderBlock block, String name, Object value) { + block.addHeader(name, value); + } + + /** + * Removes the {@code "method"} header. + */ + public static void removeMethod(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.METHOD); + } + + /** + * Returns the {@link HttpMethod} represented by the {@code "method"} header. + */ + public static HttpMethod getMethod(SpdyHeaderBlock block) { + try { + return HttpMethod.valueOf(block.getHeader(HttpNames.METHOD)); + } catch (Exception e) { + return null; + } + } + + /** + * Sets the {@code "method"} header. + */ + public static void setMethod(SpdyHeaderBlock block, HttpMethod method) { + block.setHeader(HttpNames.METHOD, method.getName()); + } + + /** + * Removes the {@code "scheme"} header. + */ + public static void removeScheme(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.SCHEME); + } + + /** + * Returns the value of the {@code "scheme"} header. + */ + public static String getScheme(SpdyHeaderBlock block) { + return block.getHeader(HttpNames.SCHEME); + } + + /** + * Sets the {@code "scheme"} header. + */ + public static void setScheme(SpdyHeaderBlock block, String value) { + block.setHeader(HttpNames.SCHEME, value); + } + + /** + * Removes the {@code "status"} header. + */ + public static void removeStatus(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.STATUS); + } + + /** + * Returns the {@link HttpResponseStatus} represented by the {@code "status"} header. + */ + public static HttpResponseStatus getStatus(SpdyHeaderBlock block) { + try { + String status = block.getHeader(HttpNames.STATUS); + int space = status.indexOf(' '); + if (space == -1) { + return HttpResponseStatus.valueOf(Integer.parseInt(status)); + } else { + int code = Integer.parseInt(status.substring(0, space)); + String reasonPhrase = status.substring(space + 1); + HttpResponseStatus responseStatus = HttpResponseStatus.valueOf(code); + if (responseStatus.getReasonPhrase().equals(responseStatus)) { + return responseStatus; + } else { + return new HttpResponseStatus(code, reasonPhrase); + } + } + } catch (Exception e) { + return null; + } + } + + /** + * Sets the {@code "status"} header. + */ + public static void setStatus(SpdyHeaderBlock block, HttpResponseStatus status) { + block.setHeader(HttpNames.STATUS, status.toString()); + } + + /** + * Removes the {@code "url"} header. + */ + public static void removeUrl(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.URL); + } + + /** + * Returns the value of the {@code "url"} header. + */ + public static String getUrl(SpdyHeaderBlock block) { + return block.getHeader(HttpNames.URL); + } + + /** + * Sets the {@code "url"} header. + */ + public static void setUrl(SpdyHeaderBlock block, String value) { + block.setHeader(HttpNames.URL, value); + } + + /** + * Removes the {@code "version"} header. + */ + public static void removeVersion(SpdyHeaderBlock block) { + block.removeHeader(HttpNames.VERSION); + } + + /** + * Returns the {@link HttpVersion} represented by the {@code "version"} header. + */ + public static HttpVersion getVersion(SpdyHeaderBlock block) { + try { + return HttpVersion.valueOf(block.getHeader(HttpNames.VERSION)); + } catch (Exception e) { + return null; + } + } + + /** + * Sets the {@code "version"} header. + */ + public static void setVersion(SpdyHeaderBlock block, HttpVersion version) { + block.setHeader(HttpNames.VERSION, version.getText()); + } + + + private static final int BUCKET_SIZE = 17; + + private static int hash(String name) { + int h = 0; + for (int i = name.length() - 1; i >= 0; i --) { + char c = name.charAt(i); + if (c >= 'A' && c <= 'Z') { + c += 32; + } + h = 31 * h + c; + } + + if (h > 0) { + return h; + } else if (h == Integer.MIN_VALUE) { + return Integer.MAX_VALUE; + } else { + return -h; + } + } + + private static boolean eq(String name1, String name2) { + int nameLen = name1.length(); + if (nameLen != name2.length()) { + return false; + } + + for (int i = nameLen - 1; i >= 0; i --) { + char c1 = name1.charAt(i); + char c2 = name2.charAt(i); + if (c1 != c2) { + if (c1 >= 'A' && c1 <= 'Z') { + c1 += 32; + } + if (c2 >= 'A' && c2 <= 'Z') { + c2 += 32; + } + if (c1 != c2) { + return false; + } + } + } + return true; + } + + private static int index(int hash) { + return hash % BUCKET_SIZE; + } + + private final Entry[] entries = new Entry[BUCKET_SIZE]; + private final Entry head = new Entry(-1, null, null); + + SpdyHeaders() { + head.before = head.after = head; + } + + void addHeader(final String name, final Object value) { + String lowerCaseName = name.toLowerCase(); + SpdyCodecUtil.validateHeaderName(lowerCaseName); + String strVal = toString(value); + SpdyCodecUtil.validateHeaderValue(strVal); + int h = hash(lowerCaseName); + int i = index(h); + addHeader0(h, i, lowerCaseName, strVal); + } + + private void addHeader0(int h, int i, final String name, final String value) { + // Update the hash table. + Entry e = entries[i]; + Entry newEntry; + entries[i] = newEntry = new Entry(h, name, value); + newEntry.next = e; + + // Update the linked list. + newEntry.addBefore(head); + } + + void removeHeader(final String name) { + if (name == null) { + throw new NullPointerException("name"); + } + String lowerCaseName = name.toLowerCase(); + int h = hash(lowerCaseName); + int i = index(h); + removeHeader0(h, i, lowerCaseName); + } + + private void removeHeader0(int h, int i, String name) { + Entry e = entries[i]; + if (e == null) { + return; + } + + for (;;) { + if (e.hash == h && eq(name, e.key)) { + e.remove(); + Entry next = e.next; + if (next != null) { + entries[i] = next; + e = next; + } else { + entries[i] = null; + return; + } + } else { + break; + } + } + + for (;;) { + Entry next = e.next; + if (next == null) { + break; + } + if (next.hash == h && eq(name, next.key)) { + e.next = next.next; + next.remove(); + } else { + e = next; + } + } + } + + void setHeader(final String name, final Object value) { + String lowerCaseName = name.toLowerCase(); + SpdyCodecUtil.validateHeaderName(lowerCaseName); + String strVal = toString(value); + SpdyCodecUtil.validateHeaderValue(strVal); + int h = hash(lowerCaseName); + int i = index(h); + removeHeader0(h, i, lowerCaseName); + addHeader0(h, i, lowerCaseName, strVal); + } + + void setHeader(final String name, final Iterable values) { + if (values == null) { + throw new NullPointerException("values"); + } + + String lowerCaseName = name.toLowerCase(); + SpdyCodecUtil.validateHeaderName(lowerCaseName); + + int h = hash(lowerCaseName); + int i = index(h); + + removeHeader0(h, i, lowerCaseName); + for (Object v: values) { + if (v == null) { + break; + } + String strVal = toString(v); + SpdyCodecUtil.validateHeaderValue(strVal); + addHeader0(h, i, lowerCaseName, strVal); + } + } + + void clearHeaders() { + for (int i = 0; i < entries.length; i ++) { + entries[i] = null; + } + head.before = head.after = head; + } + + String getHeader(final String name) { + if (name == null) { + throw new NullPointerException("name"); + } + + int h = hash(name); + int i = index(h); + Entry e = entries[i]; + while (e != null) { + if (e.hash == h && eq(name, e.key)) { + return e.value; + } + + e = e.next; + } + return null; + } + + List getHeaders(final String name) { + if (name == null) { + throw new NullPointerException("name"); + } + + LinkedList values = new LinkedList(); + + int h = hash(name); + int i = index(h); + Entry e = entries[i]; + while (e != null) { + if (e.hash == h && eq(name, e.key)) { + values.addFirst(e.value); + } + e = e.next; + } + return values; + } + + List> getHeaders() { + List> all = + new LinkedList>(); + + Entry e = head.after; + while (e != head) { + all.add(e); + e = e.after; + } + return all; + } + + boolean containsHeader(String name) { + return getHeader(name) != null; + } + + Set getHeaderNames() { + Set names = new TreeSet(); + + Entry e = head.after; + while (e != head) { + names.add(e.key); + e = e.after; + } + return names; + } + + private static String toString(Object value) { + if (value == null) { + return null; + } + return value.toString(); + } + + private static final class Entry implements Map.Entry { + final int hash; + final String key; + String value; + Entry next; + Entry before, after; + + Entry(int hash, String key, String value) { + this.hash = hash; + this.key = key; + this.value = value; + } + + void remove() { + before.after = after; + after.before = before; + } + + void addBefore(Entry e) { + after = e; + before = e.before; + before.after = this; + after.before = this; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public String setValue(String value) { + if (value == null) { + throw new NullPointerException("value"); + } + SpdyCodecUtil.validateHeaderValue(value); + String oldValue = this.value; + this.value = value; + return oldValue; + } + + @Override + public String toString() { + return key + "=" + value; + } + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeadersFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeadersFrame.java new file mode 100644 index 0000000000..38bfea96c4 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyHeadersFrame.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +/** + * A SPDY Protocol HEADERS Control Frame + */ +public interface SpdyHeadersFrame extends SpdyHeaderBlock { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyNoOpFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyNoOpFrame.java new file mode 100644 index 0000000000..4b2abbbfd7 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyNoOpFrame.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +/** + * A SPDY Protocol NOOP Control Frame + */ +public interface SpdyNoOpFrame { +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyPingFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyPingFrame.java new file mode 100644 index 0000000000..d853ff83f0 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyPingFrame.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.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/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyProtocolException.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyProtocolException.java new file mode 100644 index 0000000000..ec30acaf5b --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyProtocolException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +/** + * An {@link Exception} which is thrown when the received frame cannot + * be decoded by the {@link SpdyFrameDecoder}. + * @apiviz.exclude + */ +public class SpdyProtocolException extends Exception { + + /** + * Creates a new instance. + */ + public SpdyProtocolException() { + super(); + } + + /** + * Creates a new instance. + */ + public SpdyProtocolException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new instance. + */ + public SpdyProtocolException(String message) { + super(message); + } + + /** + * Creates a new instance. + */ + public SpdyProtocolException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyRstStreamFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyRstStreamFrame.java new file mode 100644 index 0000000000..7267f823d8 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyRstStreamFrame.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.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/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySession.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySession.java new file mode 100644 index 0000000000..9d28fcbc58 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySession.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +final class SpdySession { + + private final Map activeStreams = + new ConcurrentHashMap(); + + SpdySession() { + } + + public int numActiveStreams() { + return activeStreams.size(); + } + + public boolean noActiveStreams() { + return activeStreams.isEmpty(); + } + + public boolean isActiveStream(int streamID) { + return activeStreams.containsKey(new Integer(streamID)); + } + + public void acceptStream(int streamID, boolean remoteSideClosed, boolean localSideClosed) { + if (!remoteSideClosed || !localSideClosed) { + activeStreams.put(new Integer(streamID), + new StreamState(remoteSideClosed, localSideClosed)); + } + return; + } + + public void removeStream(int streamID) { + activeStreams.remove(new Integer(streamID)); + return; + } + + public boolean isRemoteSideClosed(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return (state == null) || state.isRemoteSideClosed(); + } + + public void closeRemoteSide(int streamID) { + Integer StreamID = new Integer(streamID); + StreamState state = activeStreams.get(StreamID); + if (state != null) { + state.closeRemoteSide(); + if (state.isLocalSideClosed()) { + activeStreams.remove(StreamID); + } + } + } + + public boolean isLocalSideClosed(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return (state == null) || state.isLocalSideClosed(); + } + + public void closeLocalSide(int streamID) { + Integer StreamID = new Integer(streamID); + StreamState state = activeStreams.get(StreamID); + if (state != null) { + state.closeLocalSide(); + if (state.isRemoteSideClosed()) { + activeStreams.remove(StreamID); + } + } + } + + public boolean hasReceivedReply(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + return (state != null) && state.hasReceivedReply(); + } + + public void receivedReply(int streamID) { + StreamState state = activeStreams.get(new Integer(streamID)); + if (state != null) { + state.receivedReply(); + } + } + + private static final class StreamState { + + private boolean remoteSideClosed; + private boolean localSideClosed; + private boolean receivedReply; + + public StreamState(boolean remoteSideClosed, boolean localSideClosed) { + this.remoteSideClosed = remoteSideClosed; + this.localSideClosed = localSideClosed; + } + + public boolean isRemoteSideClosed() { + return remoteSideClosed; + } + + public void closeRemoteSide() { + remoteSideClosed = true; + } + + public boolean isLocalSideClosed() { + return localSideClosed; + } + + public void closeLocalSide() { + localSideClosed = true; + } + + public boolean hasReceivedReply() { + return receivedReply; + } + + public void receivedReply() { + receivedReply = true; + } + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySessionHandler.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySessionHandler.java new file mode 100644 index 0000000000..53264bb6c1 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySessionHandler.java @@ -0,0 +1,471 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelDownstreamHandler; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; + +/** + * Manages streams within a SPDY session. + */ +public class SpdySessionHandler extends SimpleChannelUpstreamHandler + implements ChannelDownstreamHandler { + + private static final SpdyProtocolException PROTOCOL_EXCEPTION = new SpdyProtocolException(); + + private final SpdySession spdySession = new SpdySession(); + private volatile int lastGoodStreamID; + + private volatile int remoteConcurrentStreams; + private volatile int localConcurrentStreams; + private volatile int maxConcurrentStreams; + + private final AtomicInteger pings = new AtomicInteger(); + + private volatile boolean sentGoAwayFrame; + private volatile boolean receivedGoAwayFrame; + + private volatile ChannelFuture closeSessionFuture; + + private final boolean server; + + /** + * Creates a new session handler. + * + * @param server {@code true} if and only if this session handler should + * handle the server endpoint of the connection. + * {@code false} if and only if this session handler should + * handle the client endpoint of the connection. + */ + public SpdySessionHandler(boolean server) { + super(); + this.server = server; + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + + Object msg = e.getMessage(); + if (msg instanceof SpdyDataFrame) { + + /* + * SPDY Data frame processing requirements: + * + * If an endpoint receives a data frame for a Stream-ID which does not exist, + * it must return a RST_STREAM with error code INVALID_STREAM for the Stream-ID. + * + * If an endpoint which created the stream receives a data frame before receiving + * a SYN_REPLY on that stream, it is a protocol error, and the receiver should + * close the connection immediately. + * + * If an endpoint receives multiple data frames for invalid Stream-IDs, + * it may terminate the session. + * + * If an endpoint refuses a stream it must ignore any data frames for that stream. + * + * If an endpoint receives data on a stream which has already been torn down, + * it must ignore the data received after the teardown. + */ + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + int streamID = spdyDataFrame.getStreamID(); + + // Check if we received a data frame for a Stream-ID which is not open + if (spdySession.isRemoteSideClosed(streamID)) { + if (!sentGoAwayFrame) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.INVALID_STREAM); + } + return; + } + + // Check if we received a data frame before receiving a SYN_REPLY + if (!isRemoteInitiatedID(streamID) && !spdySession.hasReceivedReply(streamID)) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + return; + } + + if (spdyDataFrame.isLast()) { + // Close remote side of stream + halfCloseStream(streamID, true); + } + + } else if (msg instanceof SpdySynStreamFrame) { + + /* + * SPDY SYN_STREAM frame processing requirements: + * + * If an endpoint receives a SYN_STREAM with a Stream-ID that is not monotonically + * increasing, it must issue a session error with the status PROTOCOL_ERROR. + * + * If an endpoint receives multiple SYN_STREAM frames with the same active + * Stream-ID, it must issue a stream error with the status code PROTOCOL_ERROR. + */ + + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + int streamID = spdySynStreamFrame.getStreamID(); + + // Check if we received a valid SYN_STREAM frame + if (spdySynStreamFrame.isInvalid() || + !isRemoteInitiatedID(streamID) || + spdySession.isActiveStream(streamID)) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + return; + } + + // Stream-IDs must be monotonically increassing + if (streamID < lastGoodStreamID) { + issueSessionError(ctx, e.getChannel(), e.getRemoteAddress()); + return; + } + + // Try to accept the stream + boolean remoteSideClosed = spdySynStreamFrame.isLast(); + boolean localSideClosed = spdySynStreamFrame.isUnidirectional(); + if (!acceptStream(streamID, remoteSideClosed, localSideClosed)) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.REFUSED_STREAM); + return; + } + + } else if (msg instanceof SpdySynReplyFrame) { + + /* + * SPDY SYN_REPLY frame processing requirements: + * + * If an endpoint receives multiple SYN_REPLY frames for the same active Stream-ID + * it must issue a stream error with the status code PROTOCOL_ERROR. + */ + + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + int streamID = spdySynReplyFrame.getStreamID(); + + // Check if we received a valid SYN_REPLY frame + if (spdySynReplyFrame.isInvalid() || + isRemoteInitiatedID(streamID) || + spdySession.isRemoteSideClosed(streamID)) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.INVALID_STREAM); + return; + } + + // Check if we have received multiple frames for the same Stream-ID + if (spdySession.hasReceivedReply(streamID)) { + issueStreamError(ctx, e, streamID, SpdyStreamStatus.PROTOCOL_ERROR); + return; + } + + spdySession.receivedReply(streamID); + if (spdySynReplyFrame.isLast()) { + // Close remote side of stream + halfCloseStream(streamID, true); + } + + } else if (msg instanceof SpdyRstStreamFrame) { + + /* + * SPDY RST_STREAM frame processing requirements: + * + * After receiving a RST_STREAM on a stream, the receiver must not send additional + * frames on that stream. + */ + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + removeStream(spdyRstStreamFrame.getStreamID()); + + } else if (msg instanceof SpdySettingsFrame) { + + /* + * Only concerned with MAX_CONCURRENT_STREAMS + */ + + SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; + updateConcurrentStreams(spdySettingsFrame, true); + + } else if (msg instanceof SpdyPingFrame) { + + /* + * SPDY PING frame processing requirements: + * + * Receivers of a PING frame should send an identical frame to the sender + * as soon as possible. + * + * Receivers of a PING frame must ignore frames that it did not initiate + */ + + SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; + + if (isRemoteInitiatedID(spdyPingFrame.getID())) { + Channels.write(ctx, Channels.future(e.getChannel()), spdyPingFrame, e.getRemoteAddress()); + return; + } + + // Note: only checks that there are outstanding pings since uniqueness is not inforced + if (pings.get() == 0) { + return; + } + pings.getAndDecrement(); + + } else if (msg instanceof SpdyGoAwayFrame) { + + receivedGoAwayFrame = true; + + } else if (msg instanceof SpdyHeadersFrame) { + + SpdyHeadersFrame spdyHeadersFrame = (SpdyHeadersFrame) msg; + if (spdyHeadersFrame.isInvalid()) { + issueStreamError(ctx, e, spdyHeadersFrame.getStreamID(), SpdyStreamStatus.PROTOCOL_ERROR); + return; + } + } + + super.messageReceived(ctx, e); + } + + public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent evt) + throws Exception { + if (evt instanceof ChannelStateEvent) { + ChannelStateEvent e = (ChannelStateEvent) evt; + switch (e.getState()) { + case OPEN: + case CONNECTED: + case BOUND: + if (Boolean.FALSE.equals(e.getValue()) || e.getValue() == null) { + sendGoAwayFrame(ctx, e); + return; + } + } + } + if (!(evt instanceof MessageEvent)) { + ctx.sendDownstream(evt); + return; + } + + MessageEvent e = (MessageEvent) evt; + Object msg = e.getMessage(); + + if (msg instanceof SpdyDataFrame) { + + SpdyDataFrame spdyDataFrame = (SpdyDataFrame) msg; + int streamID = spdyDataFrame.getStreamID(); + + if (spdySession.isLocalSideClosed(streamID)) { + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + } + + if (spdyDataFrame.isLast()) { + halfCloseStream(streamID, false); + } + + } else if (msg instanceof SpdySynStreamFrame) { + + SpdySynStreamFrame spdySynStreamFrame = (SpdySynStreamFrame) msg; + boolean remoteSideClosed = spdySynStreamFrame.isUnidirectional(); + boolean localSideClosed = spdySynStreamFrame.isLast(); + if (!acceptStream(spdySynStreamFrame.getStreamID(), remoteSideClosed, localSideClosed)) { + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + } + + } else if (msg instanceof SpdySynReplyFrame) { + + SpdySynReplyFrame spdySynReplyFrame = (SpdySynReplyFrame) msg; + int streamID = spdySynReplyFrame.getStreamID(); + + if (!isRemoteInitiatedID(streamID) || spdySession.isLocalSideClosed(streamID)) { + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + } + + if (spdySynReplyFrame.isLast()) { + halfCloseStream(streamID, false); + } + + } else if (msg instanceof SpdyRstStreamFrame) { + + SpdyRstStreamFrame spdyRstStreamFrame = (SpdyRstStreamFrame) msg; + removeStream(spdyRstStreamFrame.getStreamID()); + + } else if (msg instanceof SpdySettingsFrame) { + + SpdySettingsFrame spdySettingsFrame = (SpdySettingsFrame) msg; + updateConcurrentStreams(spdySettingsFrame, false); + + } else if (msg instanceof SpdyPingFrame) { + + SpdyPingFrame spdyPingFrame = (SpdyPingFrame) msg; + if (isRemoteInitiatedID(spdyPingFrame.getID())) { + e.getFuture().setFailure(new IllegalArgumentException( + "invalid PING ID: " + spdyPingFrame.getID())); + return; + } + pings.getAndIncrement(); + + } else if (msg instanceof SpdyGoAwayFrame) { + + // Should send a CLOSE ChannelStateEvent + e.getFuture().setFailure(PROTOCOL_EXCEPTION); + return; + + } + + ctx.sendDownstream(evt); + } + + /* + * Error Handling + */ + + private void issueSessionError( + ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress) { + + ChannelFuture future = sendGoAwayFrame(ctx, channel, remoteAddress); + future.addListener(ChannelFutureListener.CLOSE); + } + + // Send a RST_STREAM frame in response to an incoming MessageEvent + // Only called in the upstream direction + private void issueStreamError( + ChannelHandlerContext ctx, MessageEvent e, int streamID, SpdyStreamStatus status) { + + removeStream(streamID); + SpdyRstStreamFrame spdyRstStreamFrame = new DefaultSpdyRstStreamFrame(streamID, status); + Channels.write(ctx, Channels.future(e.getChannel()), spdyRstStreamFrame, e.getRemoteAddress()); + } + + /* + * Helper functions + */ + + private boolean isServerID(int ID) { + return ID % 2 == 0; + } + + private boolean isRemoteInitiatedID(int ID) { + boolean serverID = isServerID(ID); + return (server && !serverID) || (!server && serverID); + } + + private synchronized void updateConcurrentStreams(SpdySettingsFrame settings, boolean remote) { + int newConcurrentStreams = settings.getValue(SpdySettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS); + if (newConcurrentStreams > 0) { + if (remote) { + remoteConcurrentStreams = newConcurrentStreams; + if ((localConcurrentStreams == 0) || localConcurrentStreams > remoteConcurrentStreams) { + maxConcurrentStreams = remoteConcurrentStreams; + } + } else { + localConcurrentStreams = newConcurrentStreams; + if ((remoteConcurrentStreams == 0) || remoteConcurrentStreams > localConcurrentStreams) { + maxConcurrentStreams = localConcurrentStreams; + } + } + } + } + + // need to synchronize accesses to sentGoAwayFrame and lastGoodStreamID + private synchronized boolean acceptStream( + int streamID, boolean remoteSideClosed, boolean localSideClosed) { + // Cannot initiate any new streams after receiving or sending GOAWAY + if (receivedGoAwayFrame || sentGoAwayFrame) { + return false; + } + if ((maxConcurrentStreams != 0) && + (spdySession.numActiveStreams() >= maxConcurrentStreams)) { + return false; + } + spdySession.acceptStream(streamID, remoteSideClosed, localSideClosed); + if (isRemoteInitiatedID(streamID)) { + lastGoodStreamID = streamID; + } + return true; + } + + private void halfCloseStream(int streamID, boolean remote) { + if (remote) { + spdySession.closeRemoteSide(streamID); + } else { + spdySession.closeLocalSide(streamID); + } + if ((closeSessionFuture != null) && spdySession.noActiveStreams()) { + closeSessionFuture.setSuccess(); + } + } + + private void removeStream(int streamID) { + spdySession.removeStream(streamID); + if ((closeSessionFuture != null) && spdySession.noActiveStreams()) { + closeSessionFuture.setSuccess(); + } + } + + private void sendGoAwayFrame(ChannelHandlerContext ctx, ChannelStateEvent e) { + // Avoid NotYetConnectedException + if (!e.getChannel().isConnected()) { + ctx.sendDownstream(e); + return; + } + + ChannelFuture future = sendGoAwayFrame(ctx, e.getChannel(), null); + if (spdySession.noActiveStreams()) { + future.addListener(new ClosingChannelFutureListener(ctx, e)); + } else { + closeSessionFuture = Channels.future(e.getChannel()); + closeSessionFuture.addListener(new ClosingChannelFutureListener(ctx, e)); + } + } + + private synchronized ChannelFuture sendGoAwayFrame( + ChannelHandlerContext ctx, Channel channel, SocketAddress remoteAddress) { + if (!sentGoAwayFrame) { + sentGoAwayFrame = true; + ChannelFuture future = Channels.future(channel); + Channels.write(ctx, future, new DefaultSpdyGoAwayFrame(lastGoodStreamID)); + return future; + } + return Channels.succeededFuture(channel); + } + + private static final class ClosingChannelFutureListener implements ChannelFutureListener { + + private final ChannelHandlerContext ctx; + private final ChannelStateEvent e; + + ClosingChannelFutureListener(ChannelHandlerContext ctx, ChannelStateEvent e) { + this.ctx = ctx; + this.e = e; + } + + public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception { + if (!(sentGoAwayFuture.getCause() instanceof ClosedChannelException)) { + Channels.close(ctx, e.getFuture()); + } else { + e.getFuture().setSuccess(); + } + } + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySettingsFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySettingsFrame.java new file mode 100644 index 0000000000..94f65460bd --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySettingsFrame.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +import java.util.Set; + +/** + * A SPDY Protocol SETTINGS Control Frame + */ +public interface SpdySettingsFrame { + + int SETTINGS_UPLOAD_BANDWIDTH = 1; + int SETTINGS_DOWNLOAD_BANDWIDTH = 2; + int SETTINGS_ROUND_TRIP_TIME = 3; + int SETTINGS_MAX_CONCURRENT_STREAMS = 4; + int SETTINGS_CURRENT_CWND = 5; + int SETTINGS_DOWNLOAD_RETRANS_RATE = 6; + int SETTINGS_INITIAL_WINDOW_SIZE = 7; + + /** + * Returns a {@code Set} of the setting IDs. + * The set's iterator will return the IDs in ascending order. + */ + Set getIDs(); + + /** + * Returns {@code true} if the setting ID has a value. + */ + boolean isSet(int ID); + + /** + * Returns the value of the setting ID. + * Returns -1 if the setting ID is not set. + */ + int getValue(int ID); + + /** + * Sets the value of the setting ID. + * The ID must be positive and cannot exceeed 16777215. + */ + void setValue(int ID, int value); + + /** + * Sets the value of the setting ID. + * Sets if the setting should be persisted (should only be set by the server). + * Sets if the setting is persisted (should only be set by the client). + * The ID must be positive and cannot exceed 16777215. + */ + void setValue(int ID, int value, boolean persistVal, boolean persisted); + + /** + * Removes the value of the setting ID. + * Removes all persistance information for the setting. + */ + void removeValue(int ID); + + /** + * Returns {@code true} if this setting should be persisted. + * Returns {@code false} if this setting should not be persisted + * or if the setting ID has no value. + */ + boolean persistValue(int ID); + + /** + * Sets if this setting should be persisted. + * Has no effect if the setting ID has no value. + */ + void setPersistValue(int ID, boolean persistValue); + + /** + * Returns {@code true} if this setting is persisted. + * Returns {@code false} if this setting should not be persisted + * or if the setting ID has no value. + */ + boolean isPersisted(int ID); + + /** + * Sets if this setting is persisted. + * Has no effect if the setting ID has no value. + */ + void setPersisted(int ID, boolean persisted); + + /** + * Returns {@code true} if previously persisted settings should be cleared. + */ + boolean clearPreviouslyPersistedSettings(); + + /** + * Sets if previously persisted settings should be cleared. + */ + void setClearPreviouslyPersistedSettings(boolean clear); +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyStreamStatus.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyStreamStatus.java new file mode 100644 index 0000000000..a6538c3c5a --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdyStreamStatus.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +/** + * The SPDY stream status code and its description. + * @apiviz.exclude + */ +public class SpdyStreamStatus implements Comparable { + + /** + * 1 Protocol Error + */ + public static final SpdyStreamStatus PROTOCOL_ERROR = + new SpdyStreamStatus(1, "PROTOCOL_ERROR"); + + /** + * 2 Invalid Stream + */ + public static final SpdyStreamStatus INVALID_STREAM = + new SpdyStreamStatus(2, "INVALID_STREAM"); + + /** + * 3 Refused Stream + */ + public static final SpdyStreamStatus REFUSED_STREAM = + new SpdyStreamStatus(3, "REFUSED_STREAM"); + + /** + * 4 Unsupported Version + */ + public static final SpdyStreamStatus UNSUPPORTED_VERSION = + new SpdyStreamStatus(4, "UNSUPPORTED_VERSION"); + + /** + * 5 Cancel + */ + public static final SpdyStreamStatus CANCEL = + new SpdyStreamStatus(5, "CANCEL"); + + /** + * 6 Internal Error + */ + public static final SpdyStreamStatus INTERNAL_ERROR = + new SpdyStreamStatus(6, "INTERNAL_ERROR"); + + /** + * 7 Flow Control Error + */ + public static final SpdyStreamStatus FLOW_CONTROL_ERROR = + new SpdyStreamStatus(7, "FLOW_CONTROL_ERROR"); + + /** + * Returns the {@link SpdyStreamStatus} represented by the specified code. + * If the specified code is a defined SPDY status code, a cached instance + * will be returned. Otherwise, a new instance will be returned. + */ + public static SpdyStreamStatus valueOf(int code) { + if (code == 0) { + throw new IllegalArgumentException( + "0 is not a valid status code for a RST_STREAM"); + } + + switch (code) { + case 1: + return PROTOCOL_ERROR; + case 2: + return INVALID_STREAM; + case 3: + return REFUSED_STREAM; + case 4: + return UNSUPPORTED_VERSION; + case 5: + return CANCEL; + case 6: + return INTERNAL_ERROR; + case 7: + return FLOW_CONTROL_ERROR; + } + + return new SpdyStreamStatus(code, "UNKNOWN (" + code + ')'); + } + + private final int code; + + private final String statusPhrase; + + /** + * Creates a new instance with the specified {@code code} and its + * {@code statusPhrase}. + */ + public SpdyStreamStatus(int code, String statusPhrase) { + if (code == 0) { + throw new IllegalArgumentException( + "0 is not a valid status code for a RST_STREAM"); + } + + if (statusPhrase == null) { + throw new NullPointerException("statusPhrase"); + } + + this.code = code; + this.statusPhrase = statusPhrase; + } + + /** + * Returns the code of this status. + */ + public int getCode() { + return code; + } + + /** + * Returns the status phrase of this status. + */ + public String getStatusPhrase() { + return statusPhrase; + } + + @Override + public int hashCode() { + return getCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SpdyStreamStatus)) { + return false; + } + + return getCode() == ((SpdyStreamStatus) o).getCode(); + } + + @Override + public String toString() { + return getStatusPhrase(); + } + + public int compareTo(SpdyStreamStatus o) { + return getCode() - o.getCode(); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySynReplyFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySynReplyFrame.java new file mode 100644 index 0000000000..df9e74adac --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySynReplyFrame.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.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/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySynStreamFrame.java b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySynStreamFrame.java new file mode 100644 index 0000000000..535e657480 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/SpdySynStreamFrame.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jboss.netty.handler.codec.spdy; + +/** + * A SPDY Protocol SYN_STREAM Control Frame + */ +public interface SpdySynStreamFrame extends SpdyHeaderBlock { + + /** + * Returns the Stream-ID of this frame. + */ + int getStreamID(); + + /** + * Sets the Stream-ID of this frame. The Stream-ID must be positive. + */ + void setStreamID(int streamID); + + /** + * Returns the Associated-To-Stream-ID of this frame. + */ + int getAssociatedToStreamID(); + + /** + * Sets the Associated-To-Stream-ID of this frame. + * The Associated-To-Stream-ID cannot be negative. + */ + void setAssociatedToStreamID(int associatedToStreamID); + + /** + * Returns the priority of the stream. + */ + byte getPriority(); + + /** + * Sets the priority of the stream. + * The priority must be between 0 and 3 inclusive. + */ + void setPriority(byte priority); + + /** + * Returns {@code true} if this frame is the last frame to be transmitted + * on the stream. + */ + boolean isLast(); + + /** + * Sets if this frame is the last frame to be transmitted on the stream. + */ + void setLast(boolean last); + + /** + * Returns {@code true} if the stream created with this frame is to be + * considered half-closed to the receiver. + */ + boolean isUnidirectional(); + + /** + * Sets if the stream created with this frame is to be considered + * half-closed to the receiver. + */ + void setUnidirectional(boolean unidirectional); +} diff --git a/src/main/java/org/jboss/netty/handler/codec/spdy/package-info.java b/src/main/java/org/jboss/netty/handler/codec/spdy/package-info.java new file mode 100644 index 0000000000..d7ab038cd6 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/spdy/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Encoder, decoder, session handler and their related message types for the SPDY protocol. + * + * @apiviz.exclude ^java\.lang\. + * @apiviz.exclude OneToOne(Encoder|Decoder)$ + * @apiviz.exclude \.SpdyHeaders\. + * @apiviz.exclude \.codec\.frame\. + * @apiviz.exclude \.(Simple)?Channel[A-Za-z]*Handler$ + * @apiviz.exclude \.Default + * @apiviz.exclude \.SpdyFrameCodec$ + */ +package org.jboss.netty.handler.codec.spdy;