Add WriteHeader, fix ErrShortWrite-always bug, add Framer.AllowIllegalWrites
diff --git a/frame.go b/frame.go
index 9e2a18e..223e99e 100644
--- a/frame.go
+++ b/frame.go
@@ -8,6 +8,7 @@
import (
"bytes"
"encoding/binary"
+ "errors"
"fmt"
"io"
"sync"
@@ -15,6 +16,8 @@
const frameHeaderLen = 9
+var padZeros = make([]byte, 255) // zeros for padding
+
// A FrameType is a registered frame type as defined in
// http://http2.github.io/http2-spec/#rfc.section.11.2
type FrameType uint8
@@ -203,8 +206,6 @@
}
fmt.Fprintf(&buf, " len=%d]", h.Length)
return buf.String()
- return fmt.Sprintf("[FrameHeader type=%v flags=%v stream=%v len=%v]",
- h.Type, h.Flags, h.StreamID, h.Length)
}
func (h *FrameHeader) checkValid() {
@@ -269,6 +270,14 @@
w io.Writer
wbuf []byte
+
+ // AllowIllegalWrites permits the Framer's Write methods to
+ // write frames that do not conform to the HTTP/2 spec. This
+ // permits using the Framer to test other HTTP/2
+ // implementations' conformance to the spec.
+ // If false, the Write methods will prefer to return an error
+ // rather than comply.
+ AllowIllegalWrites bool
}
func (f *Framer) startWrite(ftype FrameType, flags Flags, streamID uint32) {
@@ -279,7 +288,7 @@
0,
byte(ftype),
byte(flags),
- byte(streamID>>24), // TODO: &127? Or do it in callers? Or allow for testing.
+ byte(streamID>>24),
byte(streamID>>16),
byte(streamID>>8),
byte(streamID))
@@ -294,7 +303,7 @@
byte(length>>8),
byte(length))
n, err := f.w.Write(f.wbuf)
- if err == nil && n != length {
+ if err == nil && n != len(f.wbuf) {
err = io.ErrShortWrite
}
return err
@@ -304,6 +313,10 @@
f.wbuf = append(f.wbuf, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
+func (f *Framer) writeByte(v byte) {
+ f.wbuf = append(f.wbuf, v)
+}
+
// NewFramer returns a Framer that writes frames to w and reads them from r.
func NewFramer(w io.Writer, r io.Reader) *Framer {
return &Framer{
@@ -386,18 +399,20 @@
return f, nil
}
+var errStreamID = errors.New("invalid streamid")
+
+func validStreamID(streamID uint32) bool {
+ return streamID != 0 && streamID&(1<<31) == 0
+}
+
// WriteData writes a DATA frame.
//
// It will perform exactly one Write to the underlying Writer.
// It is the caller's responsibility to not call other Write methods concurrently.
func (f *Framer) WriteData(streamID uint32, endStream bool, data []byte) error {
// TODO: ignoring padding for now. will add when somebody cares.
- if streamID == 0 {
- // TODO: return some error to tell the caller that
- // they're doing it wrong? Or let them do as they'd
- // like? Might be useful for testing other people's
- // http2 implementations. Maybe we have a
- // Framer.AllowStupid bool?
+ if !validStreamID(streamID) && !f.AllowIllegalWrites {
+ return errStreamID
}
var flags Flags
if endStream {
@@ -570,14 +585,9 @@
type HeadersFrame struct {
FrameHeader
- // If FlagHeadersPriority:
- ExclusiveDep bool
- StreamDep uint32
+ // Priority is set if FlagHeadersPriority is set in the FrameHeader.
+ Priority PriorityParam
- // Weight is [0,255]. Only valid if FrameHeader.Flags has the
- // FlagHeadersPriority bit set, in which case the caller must
- // also add 1 to get to spec-defined [1,256] range.
- Weight uint8
headerFragBuf []byte // not owned
}
@@ -613,9 +623,9 @@
if err != nil {
return nil, err
}
- hf.StreamDep = v & 0x7fffffff
- hf.ExclusiveDep = (v != hf.StreamDep) // high bit was set
- p, hf.Weight, err = readByte(p)
+ hf.Priority.StreamDep = v & 0x7fffffff
+ hf.Priority.ExclusiveDep = (v != hf.Priority.StreamDep) // high bit was set
+ p, hf.Priority.Weight, err = readByte(p)
if err != nil {
return nil, err
}
@@ -627,6 +637,94 @@
return hf, nil
}
+// HeadersFrameParam are the parameters for writing a HEADERS frame.
+type HeadersFrameParam struct {
+ // StreamID is the required Stream ID to initiate.
+ StreamID uint32
+ // BlockFragment is part (or all) of a Header Block.
+ BlockFragment []byte
+
+ // EndStream indicates that the header block is the last that
+ // the endpoint will send for the identified stream. Setting
+ // this flag causes the stream to enter one of "half closed"
+ // states.
+ EndStream bool
+
+ // EndHeaders indicates that this frame contains an entire
+ // header block and is not followed by any
+ // CONTINUATION frames.
+ EndHeaders bool
+
+ // PadLength is the optional number of bytes of zeros to add
+ // to this frame.
+ PadLength uint8
+
+ // Priority, if non-zero, includes stream priority information
+ // in the HEADER frame.
+ Priority PriorityParam
+}
+
+// WriteHeaders writes a single HEADERS frame.
+//
+// This is a low-level header writing method. Encoding headers and
+// splitting them into any necessary CONTINUATION frames is handled
+// elsewhere.
+//
+// It will perform exactly one Write to the underlying Writer.
+// It is the caller's responsibility to not call other Write methods concurrently.
+func (f *Framer) WriteHeaders(p HeadersFrameParam) error {
+ var flags Flags
+ if p.PadLength != 0 {
+ flags |= FlagHeadersPadded
+ }
+ if p.EndStream {
+ flags |= FlagHeadersEndStream
+ }
+ if p.EndHeaders {
+ flags |= FlagHeadersEndHeaders
+ }
+ if !p.Priority.IsZero() {
+ flags |= FlagHeadersPriority
+ }
+ f.startWrite(FrameHeaders, flags, p.StreamID)
+ if p.PadLength != 0 {
+ f.writeByte(p.PadLength)
+ }
+ if !p.Priority.IsZero() {
+ v := p.Priority.StreamDep
+ if !validStreamID(v) && !f.AllowIllegalWrites {
+ return errors.New("invalid dependent stream id")
+ }
+ if p.Priority.ExclusiveDep {
+ v |= 1 << 31
+ }
+ f.writeUint32(v)
+ f.writeByte(p.Priority.Weight)
+ }
+ f.wbuf = append(f.wbuf, p.BlockFragment...)
+ f.wbuf = append(f.wbuf, padZeros[:p.PadLength]...)
+ return f.endWrite()
+}
+
+// PriorityParam are the stream prioritzation parameters.
+type PriorityParam struct {
+ // StreamDep is a 31-bit stream identifier for the
+ // stream that this stream depends on. Zero means no
+ // dependency.
+ StreamDep uint32
+
+ // ExclusiveDep is whether the dependency is exclusive.
+ ExclusiveDep bool
+
+ // Weight is the stream's weight. It should be set together
+ // with StreamDep, or neither should be set.
+ Weight uint8
+}
+
+func (p PriorityParam) IsZero() bool {
+ return p == PriorityParam{}
+}
+
// A RSTStreamFrame allows for abnormal termination of a stream.
// See http://http2.github.io/http2-spec/#rfc.section.6.4
type RSTStreamFrame struct {
@@ -646,12 +744,8 @@
// It will perform exactly one Write to the underlying Writer.
// It is the caller's responsibility to not call other Write methods concurrently.
func (f *Framer) WriteRSTStream(streamID, errCode uint32) error {
- if streamID == 0 {
- // TODO: return some error to tell the caller that
- // they're doing it wrong? Or let them do as they'd
- // like? Might be useful for testing other people's
- // http2 implementations. Maybe we have a
- // Framer.AllowStupid bool?
+ if !validStreamID(streamID) && !f.AllowIllegalWrites {
+ return errStreamID
}
f.startWrite(FrameRSTStream, 0, streamID)
f.writeUint32(errCode)
diff --git a/frame_test.go b/frame_test.go
index 2da610a..3610606 100644
--- a/frame_test.go
+++ b/frame_test.go
@@ -67,3 +67,126 @@
t.Errorf("didn't see END_STREAM flag")
}
}
+
+func TestWriteHeaders(t *testing.T) {
+ tests := []struct {
+ name string
+ p HeadersFrameParam
+ wantEnc string
+ wantFrame *HeadersFrame
+ }{
+ {
+ "basic",
+ HeadersFrameParam{
+ StreamID: 42,
+ BlockFragment: []byte("abc"),
+ Priority: PriorityParam{},
+ },
+ "\x00\x00\x03\x01\x00\x00\x00\x00*abc",
+ &HeadersFrame{
+ FrameHeader: FrameHeader{
+ valid: true,
+ StreamID: 42,
+ Type: FrameHeaders,
+ Length: uint32(len("abc")),
+ },
+ Priority: PriorityParam{},
+ headerFragBuf: []byte("abc"),
+ },
+ },
+ {
+ "basic + end flags",
+ HeadersFrameParam{
+ StreamID: 42,
+ BlockFragment: []byte("abc"),
+ EndStream: true,
+ EndHeaders: true,
+ Priority: PriorityParam{},
+ },
+ "\x00\x00\x03\x01\x05\x00\x00\x00*abc",
+ &HeadersFrame{
+ FrameHeader: FrameHeader{
+ valid: true,
+ StreamID: 42,
+ Type: FrameHeaders,
+ Flags: FlagHeadersEndStream | FlagHeadersEndHeaders,
+ Length: uint32(len("abc")),
+ },
+ Priority: PriorityParam{},
+ headerFragBuf: []byte("abc"),
+ },
+ },
+ {
+ "with padding",
+ HeadersFrameParam{
+ StreamID: 42,
+ BlockFragment: []byte("abc"),
+ EndStream: true,
+ EndHeaders: true,
+ PadLength: 5,
+ Priority: PriorityParam{},
+ },
+ "\x00\x00\t\x01\r\x00\x00\x00*\x05abc\x00\x00\x00\x00\x00",
+ &HeadersFrame{
+ FrameHeader: FrameHeader{
+ valid: true,
+ StreamID: 42,
+ Type: FrameHeaders,
+ Flags: FlagHeadersEndStream | FlagHeadersEndHeaders | FlagHeadersPadded,
+ Length: uint32(1 + len("abc") + 5), // pad length + contents + padding
+ },
+ Priority: PriorityParam{},
+ headerFragBuf: []byte("abc"),
+ },
+ },
+ {
+ "with priority",
+ HeadersFrameParam{
+ StreamID: 42,
+ BlockFragment: []byte("abc"),
+ EndStream: true,
+ EndHeaders: true,
+ PadLength: 2,
+ Priority: PriorityParam{
+ StreamDep: 15,
+ ExclusiveDep: true,
+ Weight: 127,
+ },
+ },
+ "\x00\x00\v\x01-\x00\x00\x00*\x02\x80\x00\x00\x0f\u007fabc\x00\x00",
+ &HeadersFrame{
+ FrameHeader: FrameHeader{
+ valid: true,
+ StreamID: 42,
+ Type: FrameHeaders,
+ Flags: FlagHeadersEndStream | FlagHeadersEndHeaders | FlagHeadersPadded | FlagHeadersPriority,
+ Length: uint32(1 + 5 + len("abc") + 2), // pad length + priority + contents + padding
+ },
+ Priority: PriorityParam{
+ StreamDep: 15,
+ ExclusiveDep: true,
+ Weight: 127,
+ },
+ headerFragBuf: []byte("abc"),
+ },
+ },
+ }
+ for _, tt := range tests {
+ fr, buf := testFramer()
+ if err := fr.WriteHeaders(tt.p); err != nil {
+ t.Errorf("test %q: %v", tt.name, err)
+ continue
+ }
+ if buf.String() != tt.wantEnc {
+ t.Errorf("test %q: encoded %q; want %q", tt.name, buf.Bytes(), tt.wantEnc)
+ }
+ f, err := fr.ReadFrame()
+ if err != nil {
+ t.Errorf("test %q: failed to read the frame back: %v", tt.name, err)
+ continue
+ }
+ if !reflect.DeepEqual(f, tt.wantFrame) {
+ t.Errorf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tt.name, f, tt.wantFrame)
+ }
+ }
+}