package http3
import (
"bytes"
"errors"
"fmt"
"io"
"maps"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3/qlog"
"github.com/quic-go/quic-go/qlogwriter"
"github.com/quic-go/quic-go/quicvarint"
)
type FrameType uint64
type frame any
const frameHeaderLen = 16
type countingByteReader struct {
quicvarint .Reader
NumRead int
}
func (r *countingByteReader ) ReadByte () (byte , error ) {
b , err := r .Reader .ReadByte ()
if err == nil {
r .NumRead ++
}
return b , err
}
func (r *countingByteReader ) Read (b []byte ) (int , error ) {
n , err := r .Reader .Read (b )
r .NumRead += n
return n , err
}
func (r *countingByteReader ) Reset () {
r .NumRead = 0
}
type frameParser struct {
r io .Reader
streamID quic .StreamID
closeConn func (quic .ApplicationErrorCode , string ) error
}
func (p *frameParser ) ParseNext (qlogger qlogwriter .Recorder ) (frame , error ) {
r := &countingByteReader {Reader : quicvarint .NewReader (p .r )}
for {
t , err := quicvarint .Read (r )
if err != nil {
return nil , err
}
l , err := quicvarint .Read (r )
if err != nil {
return nil , err
}
switch t {
case 0x0 :
if qlogger != nil {
qlogger .RecordEvent (qlog .FrameParsed {
StreamID : p .streamID ,
Raw : qlog .RawInfo {
Length : int (l ) + r .NumRead ,
PayloadLength : int (l ),
},
Frame : qlog .Frame {Frame : qlog .DataFrame {}},
})
}
return &dataFrame {Length : l }, nil
case 0x1 :
return &headersFrame {
Length : l ,
headerLen : r .NumRead ,
}, nil
case 0x4 :
return parseSettingsFrame (r , l , p .streamID , qlogger )
case 0x3 :
if qlogger != nil {
qlogger .RecordEvent (qlog .FrameParsed {
StreamID : p .streamID ,
Raw : qlog .RawInfo {Length : r .NumRead , PayloadLength : int (l )},
Frame : qlog .Frame {Frame : qlog .CancelPushFrame {}},
})
}
case 0x5 :
if qlogger != nil {
qlogger .RecordEvent (qlog .FrameParsed {
StreamID : p .streamID ,
Raw : qlog .RawInfo {Length : r .NumRead , PayloadLength : int (l )},
Frame : qlog .Frame {Frame : qlog .PushPromiseFrame {}},
})
}
case 0x7 :
return parseGoAwayFrame (r , l , p .streamID , qlogger )
case 0xd :
if qlogger != nil {
qlogger .RecordEvent (qlog .FrameParsed {
StreamID : p .streamID ,
Raw : qlog .RawInfo {Length : r .NumRead , PayloadLength : int (l )},
Frame : qlog .Frame {Frame : qlog .MaxPushIDFrame {}},
})
}
case 0x2 , 0x6 , 0x8 , 0x9 :
if qlogger != nil {
qlogger .RecordEvent (qlog .FrameParsed {
StreamID : p .streamID ,
Raw : qlog .RawInfo {Length : r .NumRead + int (l ), PayloadLength : int (l )},
Frame : qlog .Frame {Frame : qlog .ReservedFrame {Type : t }},
})
}
p .closeConn (quic .ApplicationErrorCode (ErrCodeFrameUnexpected ), "" )
return nil , fmt .Errorf ("http3: reserved frame type: %d" , t )
default :
if qlogger != nil {
qlogger .RecordEvent (qlog .FrameParsed {
StreamID : p .streamID ,
Raw : qlog .RawInfo {Length : r .NumRead , PayloadLength : int (l )},
Frame : qlog .Frame {Frame : qlog .UnknownFrame {Type : t }},
})
}
}
if _ , err := io .CopyN (io .Discard , r , int64 (l )); err != nil {
return nil , err
}
r .Reset ()
}
}
type dataFrame struct {
Length uint64
}
func (f *dataFrame ) Append (b []byte ) []byte {
b = quicvarint .Append (b , 0x0 )
return quicvarint .Append (b , f .Length )
}
type headersFrame struct {
Length uint64
headerLen int
}
func (f *headersFrame ) Append (b []byte ) []byte {
b = quicvarint .Append (b , 0x1 )
return quicvarint .Append (b , f .Length )
}
const (
settingMaxFieldSectionSize = 0x6
settingExtendedConnect = 0x8
settingDatagram = 0x33
)
type settingsFrame struct {
MaxFieldSectionSize int64
Datagram bool
ExtendedConnect bool
Other map [uint64 ]uint64
}
func pointer[T any ](v T ) *T {
return &v
}
func parseSettingsFrame(r *countingByteReader , l uint64 , streamID quic .StreamID , qlogger qlogwriter .Recorder ) (*settingsFrame , error ) {
if l > 8 *(1 <<10 ) {
return nil , fmt .Errorf ("unexpected size for SETTINGS frame: %d" , l )
}
buf := make ([]byte , l )
if _ , err := io .ReadFull (r , buf ); err != nil {
if err == io .ErrUnexpectedEOF {
return nil , io .EOF
}
return nil , err
}
frame := &settingsFrame {MaxFieldSectionSize : -1 }
b := bytes .NewReader (buf )
settingsFrame := qlog .SettingsFrame {MaxFieldSectionSize : -1 }
var readMaxFieldSectionSize , readDatagram , readExtendedConnect bool
for b .Len () > 0 {
id , err := quicvarint .Read (b )
if err != nil {
return nil , err
}
val , err := quicvarint .Read (b )
if err != nil {
return nil , err
}
switch id {
case settingMaxFieldSectionSize :
if readMaxFieldSectionSize {
return nil , fmt .Errorf ("duplicate setting: %d" , id )
}
readMaxFieldSectionSize = true
frame .MaxFieldSectionSize = int64 (val )
settingsFrame .MaxFieldSectionSize = int64 (val )
case settingExtendedConnect :
if readExtendedConnect {
return nil , fmt .Errorf ("duplicate setting: %d" , id )
}
readExtendedConnect = true
if val != 0 && val != 1 {
return nil , fmt .Errorf ("invalid value for SETTINGS_ENABLE_CONNECT_PROTOCOL: %d" , val )
}
frame .ExtendedConnect = val == 1
if qlogger != nil {
settingsFrame .ExtendedConnect = pointer (frame .ExtendedConnect )
}
case settingDatagram :
if readDatagram {
return nil , fmt .Errorf ("duplicate setting: %d" , id )
}
readDatagram = true
if val != 0 && val != 1 {
return nil , fmt .Errorf ("invalid value for SETTINGS_H3_DATAGRAM: %d" , val )
}
frame .Datagram = val == 1
if qlogger != nil {
settingsFrame .Datagram = pointer (frame .Datagram )
}
default :
if _ , ok := frame .Other [id ]; ok {
return nil , fmt .Errorf ("duplicate setting: %d" , id )
}
if frame .Other == nil {
frame .Other = make (map [uint64 ]uint64 )
}
frame .Other [id ] = val
}
}
if qlogger != nil {
settingsFrame .Other = maps .Clone (frame .Other )
qlogger .RecordEvent (qlog .FrameParsed {
StreamID : streamID ,
Raw : qlog .RawInfo {
Length : r .NumRead ,
PayloadLength : int (l ),
},
Frame : qlog .Frame {Frame : settingsFrame },
})
}
return frame , nil
}
func (f *settingsFrame ) Append (b []byte ) []byte {
b = quicvarint .Append (b , 0x4 )
var l int
if f .MaxFieldSectionSize >= 0 {
l += quicvarint .Len (settingMaxFieldSectionSize ) + quicvarint .Len (uint64 (f .MaxFieldSectionSize ))
}
for id , val := range f .Other {
l += quicvarint .Len (id ) + quicvarint .Len (val )
}
if f .Datagram {
l += quicvarint .Len (settingDatagram ) + quicvarint .Len (1 )
}
if f .ExtendedConnect {
l += quicvarint .Len (settingExtendedConnect ) + quicvarint .Len (1 )
}
b = quicvarint .Append (b , uint64 (l ))
if f .MaxFieldSectionSize >= 0 {
b = quicvarint .Append (b , settingMaxFieldSectionSize )
b = quicvarint .Append (b , uint64 (f .MaxFieldSectionSize ))
}
if f .Datagram {
b = quicvarint .Append (b , settingDatagram )
b = quicvarint .Append (b , 1 )
}
if f .ExtendedConnect {
b = quicvarint .Append (b , settingExtendedConnect )
b = quicvarint .Append (b , 1 )
}
for id , val := range f .Other {
b = quicvarint .Append (b , id )
b = quicvarint .Append (b , val )
}
return b
}
type goAwayFrame struct {
StreamID quic .StreamID
}
func parseGoAwayFrame(r *countingByteReader , l uint64 , streamID quic .StreamID , qlogger qlogwriter .Recorder ) (*goAwayFrame , error ) {
frame := &goAwayFrame {}
startLen := r .NumRead
id , err := quicvarint .Read (r )
if err != nil {
return nil , err
}
if r .NumRead -startLen != int (l ) {
return nil , errors .New ("GOAWAY frame: inconsistent length" )
}
frame .StreamID = quic .StreamID (id )
if qlogger != nil {
qlogger .RecordEvent (qlog .FrameParsed {
StreamID : streamID ,
Raw : qlog .RawInfo {Length : r .NumRead , PayloadLength : int (l )},
Frame : qlog .Frame {Frame : qlog .GoAwayFrame {StreamID : frame .StreamID }},
})
}
return frame , nil
}
func (f *goAwayFrame ) Append (b []byte ) []byte {
b = quicvarint .Append (b , 0x7 )
b = quicvarint .Append (b , uint64 (quicvarint .Len (uint64 (f .StreamID ))))
return quicvarint .Append (b , uint64 (f .StreamID ))
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .