// Copyright 2025 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http3 import ( "context" "io" "os" "sync" "time" "golang.org/x/net/quic" ) // A stream wraps a QUIC stream, providing methods to read/write various values. type stream struct { stream *quic.Stream // lim is the current read limit. // Reading a frame header sets the limit to the end of the frame. // Reading past the limit or reading less than the limit and ending the frame // results in an error. // -1 indicates no limit. lim int64 readDeadline deadline writeDeadline deadline } // newConnStream creates a new stream on a connection. // It writes the stream header for unidirectional streams. // // The stream returned by newStream is not flushed, // and will not be sent to the peer until the caller calls // Flush or writes enough data to the stream. func newConnStream(ctx context.Context, qconn *quic.Conn, stype streamType) (*stream, error) { var qs *quic.Stream var err error if stype == streamTypeRequest { // Request streams are bidirectional. qs, err = qconn.NewStream(ctx) } else { // All other streams are unidirectional. qs, err = qconn.NewSendOnlyStream(ctx) } if err != nil { return nil, err } st := newStream(qs) if stype != streamTypeRequest { // Unidirectional stream header. st.writeVarint(int64(stype)) } return st, err } func newStream(qs *quic.Stream) *stream { readCtx, readCancel := context.WithCancelCause(context.Background()) writeCtx, writeCancel := context.WithCancelCause(context.Background()) st := &stream{ stream: qs, lim: -1, // no limit readDeadline: deadline{ ctx: readCtx, cancel: readCancel, }, writeDeadline: deadline{ ctx: writeCtx, cancel: writeCancel, }, } qs.SetReadContext(readCtx) qs.SetWriteContext(writeCtx) return st } func (st *stream) Close() error { st.readDeadline.stop() st.writeDeadline.stop() return st.stream.Close() } func (st *stream) CloseRead() { st.readDeadline.stop() st.stream.CloseRead() } func (st *stream) CloseWrite() { st.writeDeadline.stop() st.stream.CloseWrite() } func (st *stream) Reset(code uint64) { st.readDeadline.stop() st.writeDeadline.stop() st.stream.Reset(code) } // deadline manages ctx, and cancels it when timer expires, with // [os.ErrDeadlineExceeded] as the cause. If the deadline is manually stopped // before timer expires, the context will be canceled with [context.Canceled] // as the cause. Once a deadline is exceeded, its timer can no longer be // extended. // Practically, this lets the http3 package support time-based deadlines by // utilizing the quic package's support for context-based deadlines. type deadline struct { ctx context.Context cancel context.CancelCauseFunc mu sync.Mutex // Guards below. timer *time.Timer } // stopTimerLocked stops the deadline timer and sets it to nil. // The caller must hold d.mu. func (d *deadline) stopTimerLocked() { if d.timer != nil { d.timer.Stop() d.timer = nil } } // stop stops the deadline timer and cancels the context with // [context.Canceled] as the cause. func (d *deadline) stop() { d.mu.Lock() d.stopTimerLocked() d.mu.Unlock() d.cancel(context.Canceled) } // err returns the deadline's context cancelation cause, if any. func (d *deadline) err() error { return context.Cause(d.ctx) } // errOf returns the deadline's context cancelation cause if the given err is // non-nil. This can be used to check whether an error value returned by I/O // operations at the QUIC layer is non-nil because the deadline has expired. func (d *deadline) errOf(err error) error { if dErr := d.err(); err != nil && dErr != nil { return dErr } return err } // set configures a new deadline using the given deadlineTime. // Once deadline is exceeded, it remains in the expired (sticky) state, and // subsequent attempts to extend or reset the deadline are ignored. func (d *deadline) set(deadlineTime time.Time) { d.mu.Lock() defer d.mu.Unlock() if d.ctx.Err() != nil { // Already expired, sticky error. return } if deadlineTime.IsZero() { d.stopTimerLocked() return } dur := time.Until(deadlineTime) if dur <= 0 { d.stopTimerLocked() d.cancel(os.ErrDeadlineExceeded) return } if d.timer == nil { d.timer = time.AfterFunc(dur, func() { d.cancel(os.ErrDeadlineExceeded) }) } else { d.timer.Reset(dur) } } // readFrameHeader reads the type and length fields of an HTTP/3 frame. // It sets the read limit to the end of the frame. // // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.1 func (st *stream) readFrameHeader() (ftype frameType, err error) { if st.lim >= 0 { // We shouldn't call readFrameHeader before ending the previous frame. return 0, errH3FrameError } ftype, err = readVarint[frameType](st) if err != nil { return 0, err } size, err := st.readVarint() if err != nil { return 0, err } st.lim = size return ftype, nil } // endFrame is called after reading a frame to reset the read limit. // It returns an error if the entire contents of a frame have not been read. func (st *stream) endFrame() error { if st.lim != 0 { return &connectionError{ code: errH3FrameError, message: "invalid HTTP/3 frame", } } st.lim = -1 return nil } // readFrameData returns the remaining data in the current frame. func (st *stream) readFrameData() ([]byte, error) { if st.lim < 0 { return nil, errH3FrameError } // TODO: Pool buffers to avoid allocation here. b := make([]byte, st.lim) _, err := io.ReadFull(st, b) if err != nil { return nil, err } return b, nil } // ReadByte reads one byte from the stream. func (st *stream) ReadByte() (b byte, err error) { // Check the deadline before doing I/O operations on the QUIC layer. We do // this because the QUIC layer implements a fast path for I/O operations, // allowing Read & Write to succeed depending on the state of buffer, even // if its context has been canceled. By always checking the deadline here, // we make it so that I/O operations fail as soon as its relevant deadline // has been exceeded. if err := st.readDeadline.err(); err != nil { return 0, err } if err := st.recordBytesRead(1); err != nil { return 0, err } b, err = st.stream.ReadByte() if err == io.EOF && st.lim >= 0 { return 0, errH3FrameError } return b, st.readDeadline.errOf(err) } // Read reads from the stream. func (st *stream) Read(b []byte) (int, error) { // Check the deadline before doing I/O operations on the QUIC layer. We do // this because the QUIC layer implements a fast path for I/O operations, // allowing Read & Write to succeed depending on the state of buffer, even // if its context has been canceled. By always checking the deadline here, // we make it so that I/O operations fail as soon as its relevant deadline // has been exceeded. if err := st.readDeadline.err(); err != nil { return 0, err } n, err := st.stream.Read(b) if e2 := st.recordBytesRead(n); e2 != nil { return 0, e2 } if err == io.EOF { if st.lim == 0 { // EOF at end of frame, ignore. return n, nil } else if st.lim > 0 { // EOF inside frame, error. return 0, errH3FrameError } else { // EOF outside of frame, surface to caller. return n, io.EOF } } return n, st.readDeadline.errOf(err) } // discardUnknownFrame discards an unknown frame. // // HTTP/3 requires that unknown frames be ignored on all streams. // However, a known frame appearing in an unexpected place is a fatal error, // so this returns an error if the frame is one we know. func (st *stream) discardUnknownFrame(ftype frameType) error { switch ftype { case frameTypeData, frameTypeHeaders, frameTypeCancelPush, frameTypeSettings, frameTypePushPromise, frameTypeGoaway, frameTypeMaxPushID: return &connectionError{ code: errH3FrameUnexpected, message: "unexpected " + ftype.String() + " frame", } } return st.discardFrame() } // discardFrame discards any remaining data in the current frame and resets the read limit. func (st *stream) discardFrame() error { // TODO: Consider adding a *quic.Stream method to discard some amount of data. for range st.lim { _, err := st.ReadByte() if err != nil { return &streamError{errH3FrameError, err.Error()} } } st.lim = -1 return nil } // Write writes to the stream. func (st *stream) Write(b []byte) (int, error) { // Check the deadline before doing I/O operations on the QUIC layer. We do // this because the QUIC layer implements a fast path for I/O operations, // allowing Read & Write to succeed depending on the state of buffer, even // if its context has been canceled. By always checking the deadline here, // we make it so that I/O operations fail as soon as its relevant deadline // has been exceeded. if err := st.writeDeadline.err(); err != nil { return 0, err } n, err := st.stream.Write(b) return n, st.writeDeadline.errOf(err) } // Flush commits data written to the stream. func (st *stream) Flush() error { // Check the deadline before doing I/O operations on the QUIC layer. We do // this because the QUIC layer implements a fast path for I/O operations, // allowing Read & Write to succeed depending on the state of buffer, even // if its context has been canceled. By always checking the deadline here, // we make it so that I/O operations fail as soon as its relevant deadline // has been exceeded. if err := st.writeDeadline.err(); err != nil { return err } return st.writeDeadline.errOf(st.stream.Flush()) } // WriteByte writes one byte to the stream. func (st *stream) WriteByte(c byte) error { // Check the deadline before doing I/O operations on the QUIC layer. We do // this because the QUIC layer implements a fast path for I/O operations, // allowing Read & Write to succeed depending on the state of buffer, even // if its context has been canceled. By always checking the deadline here, // we make it so that I/O operations fail as soon as its relevant deadline // has been exceeded. if err := st.writeDeadline.err(); err != nil { return err } return st.writeDeadline.errOf(st.stream.WriteByte(c)) } // readVarint reads a QUIC variable-length integer from the stream. func (st *stream) readVarint() (v int64, err error) { b, err := st.ReadByte() if err != nil { return 0, err } v = int64(b & 0x3f) n := 1 << (b >> 6) for i := 1; i < n; i++ { b, err := st.ReadByte() if err != nil { if err == io.EOF { return 0, errH3FrameError } return 0, err } v = (v << 8) | int64(b) } return v, nil } // readVarint reads a varint of a particular type. func readVarint[T ~int64 | ~uint64](st *stream) (T, error) { v, err := st.readVarint() return T(v), err } // writeVarint writes a QUIC variable-length integer to the stream. func (st *stream) writeVarint(v int64) { switch { case v <= (1<<6)-1: st.WriteByte(byte(v)) case v <= (1<<14)-1: st.WriteByte((1 << 6) | byte(v>>8)) st.WriteByte(byte(v)) case v <= (1<<30)-1: st.WriteByte((2 << 6) | byte(v>>24)) st.WriteByte(byte(v >> 16)) st.WriteByte(byte(v >> 8)) st.WriteByte(byte(v)) case v <= (1<<62)-1: st.WriteByte((3 << 6) | byte(v>>56)) st.WriteByte(byte(v >> 48)) st.WriteByte(byte(v >> 40)) st.WriteByte(byte(v >> 32)) st.WriteByte(byte(v >> 24)) st.WriteByte(byte(v >> 16)) st.WriteByte(byte(v >> 8)) st.WriteByte(byte(v)) default: panic("varint too large") } } // recordBytesRead records that n bytes have been read. // It returns an error if the read passes the current limit. func (st *stream) recordBytesRead(n int) error { if st.lim < 0 { return nil } st.lim -= int64(n) if st.lim < 0 { st.stream = nil // panic if we try to read again return &connectionError{ code: errH3FrameError, message: "invalid HTTP/3 frame", } } return nil }