| // 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 |
| } |