package http3
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"time"
"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/qpack"
)
type datagramStream interface {
io .ReadWriteCloser
CancelRead(quic .StreamErrorCode )
CancelWrite(quic .StreamErrorCode )
StreamID() quic .StreamID
Context() context .Context
SetDeadline(time .Time ) error
SetReadDeadline(time .Time ) error
SetWriteDeadline(time .Time ) error
SendDatagram(b []byte ) error
ReceiveDatagram(ctx context .Context ) ([]byte , error )
QUICStream() *quic .Stream
}
type Stream struct {
datagramStream
conn *rawConn
frameParser *frameParser
buf []byte
bytesRemainingInFrame uint64
qlogger qlogwriter .Recorder
parseTrailer func (io .Reader , *headersFrame ) error
parsedTrailer bool
}
func newStream(
str datagramStream ,
conn *rawConn ,
trace *httptrace .ClientTrace ,
parseTrailer func (io .Reader , *headersFrame ) error ,
qlogger qlogwriter .Recorder ,
) *Stream {
return &Stream {
datagramStream : str ,
conn : conn ,
buf : make ([]byte , 16 ),
qlogger : qlogger ,
parseTrailer : parseTrailer ,
frameParser : &frameParser {
r : &tracingReader {Reader : str , trace : trace },
streamID : str .StreamID (),
closeConn : conn .CloseWithError ,
},
}
}
func (s *Stream ) Read (b []byte ) (int , error ) {
if s .bytesRemainingInFrame == 0 {
parseLoop :
for {
frame , err := s .frameParser .ParseNext (s .qlogger )
if err != nil {
return 0 , err
}
switch f := frame .(type ) {
case *dataFrame :
if s .parsedTrailer {
return 0 , errors .New ("DATA frame received after trailers" )
}
s .bytesRemainingInFrame = f .Length
break parseLoop
case *headersFrame :
if s .parsedTrailer {
maybeQlogInvalidHeadersFrame (s .qlogger , s .StreamID (), f .Length )
return 0 , errors .New ("additional HEADERS frame received after trailers" )
}
s .parsedTrailer = true
return 0 , s .parseTrailer (s .datagramStream , f )
default :
s .conn .CloseWithError (quic .ApplicationErrorCode (ErrCodeFrameUnexpected ), "" )
return 0 , fmt .Errorf ("peer sent an unexpected frame: %T" , f )
}
}
}
var n int
var err error
if s .bytesRemainingInFrame < uint64 (len (b )) {
n , err = s .datagramStream .Read (b [:s .bytesRemainingInFrame ])
} else {
n , err = s .datagramStream .Read (b )
}
s .bytesRemainingInFrame -= uint64 (n )
return n , err
}
func (s *Stream ) hasMoreData () bool {
return s .bytesRemainingInFrame > 0
}
func (s *Stream ) Write (b []byte ) (int , error ) {
s .buf = s .buf [:0 ]
s .buf = (&dataFrame {Length : uint64 (len (b ))}).Append (s .buf )
if s .qlogger != nil {
s .qlogger .RecordEvent (qlog .FrameCreated {
StreamID : s .StreamID (),
Raw : qlog .RawInfo {
Length : len (s .buf ) + len (b ),
PayloadLength : len (b ),
},
Frame : qlog .Frame {Frame : qlog .DataFrame {}},
})
}
if _ , err := s .datagramStream .Write (s .buf ); err != nil {
return 0 , err
}
return s .datagramStream .Write (b )
}
func (s *Stream ) writeUnframed (b []byte ) (int , error ) {
return s .datagramStream .Write (b )
}
func (s *Stream ) StreamID () quic .StreamID {
return s .datagramStream .StreamID ()
}
func (s *Stream ) SendDatagram (b []byte ) error {
return s .datagramStream .SendDatagram (b )
}
func (s *Stream ) ReceiveDatagram (ctx context .Context ) ([]byte , error ) {
return s .datagramStream .ReceiveDatagram (ctx )
}
type RequestStream struct {
str *Stream
responseBody io .ReadCloser
decoder *qpack .Decoder
requestWriter *requestWriter
maxHeaderBytes int
reqDone chan <- struct {}
disableCompression bool
response *http .Response
sentRequest bool
requestedGzip bool
isConnect bool
}
func newRequestStream(
str *Stream ,
requestWriter *requestWriter ,
reqDone chan <- struct {},
decoder *qpack .Decoder ,
disableCompression bool ,
maxHeaderBytes int ,
rsp *http .Response ,
) *RequestStream {
return &RequestStream {
str : str ,
requestWriter : requestWriter ,
reqDone : reqDone ,
decoder : decoder ,
disableCompression : disableCompression ,
maxHeaderBytes : maxHeaderBytes ,
response : rsp ,
}
}
func (s *RequestStream ) Read (b []byte ) (int , error ) {
if s .responseBody == nil {
return 0 , errors .New ("http3: invalid use of RequestStream.Read before ReadResponse" )
}
return s .responseBody .Read (b )
}
func (s *RequestStream ) StreamID () quic .StreamID {
return s .str .StreamID ()
}
func (s *RequestStream ) Write (b []byte ) (int , error ) {
if !s .sentRequest {
return 0 , errors .New ("http3: invalid use of RequestStream.Write before SendRequestHeader" )
}
return s .str .Write (b )
}
func (s *RequestStream ) Close () error {
return s .str .Close ()
}
func (s *RequestStream ) CancelRead (errorCode quic .StreamErrorCode ) {
s .str .CancelRead (errorCode )
}
func (s *RequestStream ) CancelWrite (errorCode quic .StreamErrorCode ) {
s .str .CancelWrite (errorCode )
}
func (s *RequestStream ) Context () context .Context {
return s .str .Context ()
}
func (s *RequestStream ) SetReadDeadline (t time .Time ) error {
return s .str .SetReadDeadline (t )
}
func (s *RequestStream ) SetWriteDeadline (t time .Time ) error {
return s .str .SetWriteDeadline (t )
}
func (s *RequestStream ) SetDeadline (t time .Time ) error {
return s .str .SetDeadline (t )
}
func (s *RequestStream ) SendDatagram (b []byte ) error {
return s .str .SendDatagram (b )
}
func (s *RequestStream ) ReceiveDatagram (ctx context .Context ) ([]byte , error ) {
return s .str .ReceiveDatagram (ctx )
}
func (s *RequestStream ) SendRequestHeader (req *http .Request ) error {
if req .Body != nil && req .Body != http .NoBody {
return errors .New ("http3: invalid use of RequestStream.SendRequestHeader with a request that has a request body" )
}
return s .sendRequestHeader (req )
}
func (s *RequestStream ) sendRequestHeader (req *http .Request ) error {
if s .sentRequest {
return errors .New ("http3: invalid duplicate use of RequestStream.SendRequestHeader" )
}
if !s .disableCompression && req .Method != http .MethodHead &&
req .Header .Get ("Accept-Encoding" ) == "" && req .Header .Get ("Range" ) == "" {
s .requestedGzip = true
}
s .isConnect = req .Method == http .MethodConnect
s .sentRequest = true
return s .requestWriter .WriteRequestHeader (s .str .datagramStream , req , s .requestedGzip , s .str .StreamID (), s .str .qlogger )
}
func (s *RequestStream ) sendRequestTrailer (req *http .Request ) error {
return s .requestWriter .WriteRequestTrailer (s .str .datagramStream , req , s .str .StreamID (), s .str .qlogger )
}
func (s *RequestStream ) ReadResponse () (*http .Response , error ) {
if !s .sentRequest {
return nil , errors .New ("http3: invalid use of RequestStream.ReadResponse before SendRequestHeader" )
}
frame , err := s .str .frameParser .ParseNext (s .str .qlogger )
if err != nil {
s .str .CancelRead (quic .StreamErrorCode (ErrCodeFrameError ))
s .str .CancelWrite (quic .StreamErrorCode (ErrCodeFrameError ))
return nil , fmt .Errorf ("http3: parsing frame failed: %w" , err )
}
hf , ok := frame .(*headersFrame )
if !ok {
s .str .conn .CloseWithError (quic .ApplicationErrorCode (ErrCodeFrameUnexpected ), "expected first frame to be a HEADERS frame" )
return nil , errors .New ("http3: expected first frame to be a HEADERS frame" )
}
if hf .Length > uint64 (s .maxHeaderBytes ) {
maybeQlogInvalidHeadersFrame (s .str .qlogger , s .str .StreamID (), hf .Length )
s .str .CancelRead (quic .StreamErrorCode (ErrCodeFrameError ))
s .str .CancelWrite (quic .StreamErrorCode (ErrCodeFrameError ))
return nil , fmt .Errorf ("http3: HEADERS frame too large: %d bytes (max: %d)" , hf .Length , s .maxHeaderBytes )
}
headerBlock := make ([]byte , hf .Length )
if _ , err := io .ReadFull (s .str .datagramStream , headerBlock ); err != nil {
maybeQlogInvalidHeadersFrame (s .str .qlogger , s .str .StreamID (), hf .Length )
s .str .CancelRead (quic .StreamErrorCode (ErrCodeRequestIncomplete ))
s .str .CancelWrite (quic .StreamErrorCode (ErrCodeRequestIncomplete ))
return nil , fmt .Errorf ("http3: failed to read response headers: %w" , err )
}
decodeFn := s .decoder .Decode (headerBlock )
var hfs []qpack .HeaderField
if s .str .qlogger != nil {
hfs = make ([]qpack .HeaderField , 0 , 16 )
}
res := s .response
err = updateResponseFromHeaders (res , decodeFn , s .maxHeaderBytes , &hfs )
if s .str .qlogger != nil {
qlogParsedHeadersFrame (s .str .qlogger , s .str .StreamID (), hf , hfs )
}
if err != nil {
errCode := ErrCodeMessageError
var qpackErr *qpackError
if errors .As (err , &qpackErr ) {
errCode = ErrCodeQPACKDecompressionFailed
}
s .str .CancelRead (quic .StreamErrorCode (errCode ))
s .str .CancelWrite (quic .StreamErrorCode (errCode ))
return nil , fmt .Errorf ("http3: invalid response: %w" , err )
}
respBody := newResponseBody (s .str , res .ContentLength , s .reqDone )
isInformational := res .StatusCode >= 100 && res .StatusCode < 200
isNoContent := res .StatusCode == http .StatusNoContent
isSuccessfulConnect := s .isConnect && res .StatusCode >= 200 && res .StatusCode < 300
if (isInformational || isNoContent || isSuccessfulConnect ) && res .ContentLength == -1 {
res .ContentLength = 0
}
if s .requestedGzip && res .Header .Get ("Content-Encoding" ) == "gzip" {
res .Header .Del ("Content-Encoding" )
res .Header .Del ("Content-Length" )
res .ContentLength = -1
s .responseBody = newGzipReader (respBody )
res .Uncompressed = true
} else {
s .responseBody = respBody
}
res .Body = s .responseBody
return res , nil
}
type tracingReader struct {
io .Reader
readFirst bool
trace *httptrace .ClientTrace
}
func (r *tracingReader ) Read (b []byte ) (int , error ) {
n , err := r .Reader .Read (b )
if n > 0 && !r .readFirst {
traceGotFirstResponseByte (r .trace )
r .readFirst = true
}
return n , err
}
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 .