package http3
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/qpack"
)
type Stream interface {
quic .Stream
SendDatagram ([]byte ) error
ReceiveDatagram (context .Context ) ([]byte , error )
}
type RequestStream interface {
Stream
SendRequestHeader (req *http .Request ) error
ReadResponse () (*http .Response , error )
}
type stream struct {
quic .Stream
conn *connection
buf []byte
bytesRemainingInFrame uint64
datagrams *datagrammer
parseTrailer func (io .Reader , uint64 ) error
parsedTrailer bool
}
var _ Stream = &stream {}
func newStream(str quic .Stream , conn *connection , datagrams *datagrammer , parseTrailer func (io .Reader , uint64 ) error ) *stream {
return &stream {
Stream : str ,
conn : conn ,
buf : make ([]byte , 16 ),
datagrams : datagrams ,
parseTrailer : parseTrailer ,
}
}
func (s *stream ) Read (b []byte ) (int , error ) {
fp := &frameParser {
r : s .Stream ,
conn : s .conn ,
}
if s .bytesRemainingInFrame == 0 {
parseLoop :
for {
frame , err := fp .ParseNext ()
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 .conn .perspective == protocol .PerspectiveServer {
continue
}
if s .parsedTrailer {
return 0 , errors .New ("additional HEADERS frame received after trailers" )
}
s .parsedTrailer = true
return 0 , s .parseTrailer (s .Stream , f .Length )
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 .Stream .Read (b [:s .bytesRemainingInFrame ])
} else {
n , err = s .Stream .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 _ , err := s .Stream .Write (s .buf ); err != nil {
return 0 , err
}
return s .Stream .Write (b )
}
func (s *stream ) writeUnframed (b []byte ) (int , error ) {
return s .Stream .Write (b )
}
func (s *stream ) StreamID () protocol .StreamID {
return s .Stream .StreamID ()
}
type requestStream struct {
*stream
responseBody io .ReadCloser
decoder *qpack .Decoder
requestWriter *requestWriter
maxHeaderBytes uint64
reqDone chan <- struct {}
disableCompression bool
response *http .Response
trace *httptrace .ClientTrace
sentRequest bool
requestedGzip bool
isConnect bool
firstByte bool
}
var _ RequestStream = &requestStream {}
func newRequestStream(
str *stream ,
requestWriter *requestWriter ,
reqDone chan <- struct {},
decoder *qpack .Decoder ,
disableCompression bool ,
maxHeaderBytes uint64 ,
rsp *http .Response ,
trace *httptrace .ClientTrace ,
) *requestStream {
return &requestStream {
stream : str ,
requestWriter : requestWriter ,
reqDone : reqDone ,
decoder : decoder ,
disableCompression : disableCompression ,
maxHeaderBytes : maxHeaderBytes ,
response : rsp ,
trace : trace ,
}
}
func (s *requestStream ) Read (b []byte ) (int , error ) {
if s .responseBody == nil {
return 0 , errors .New ("http3: invalid use of RequestStream.Read: need to call ReadResponse first" )
}
return s .responseBody .Read (b )
}
func (s *requestStream ) SendRequestHeader (req *http .Request ) error {
if s .sentRequest {
return errors .New ("http3: invalid duplicate use of 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 .Stream , req , s .requestedGzip )
}
func (s *requestStream ) ReadResponse () (*http .Response , error ) {
fp := &frameParser {
conn : s .conn ,
r : &tracingReader {
Reader : s .Stream ,
first : &s .firstByte ,
trace : s .trace ,
},
}
frame , err := fp .ParseNext ()
if err != nil {
s .CancelRead (quic .StreamErrorCode (ErrCodeFrameError ))
s .CancelWrite (quic .StreamErrorCode (ErrCodeFrameError ))
return nil , fmt .Errorf ("http3: parsing frame failed: %w" , err )
}
hf , ok := frame .(*headersFrame )
if !ok {
s .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 > s .maxHeaderBytes {
s .CancelRead (quic .StreamErrorCode (ErrCodeFrameError ))
s .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 .Stream , headerBlock ); err != nil {
s .CancelRead (quic .StreamErrorCode (ErrCodeRequestIncomplete ))
s .CancelWrite (quic .StreamErrorCode (ErrCodeRequestIncomplete ))
return nil , fmt .Errorf ("http3: failed to read response headers: %w" , err )
}
hfs , err := s .decoder .DecodeFull (headerBlock )
if err != nil {
s .conn .CloseWithError (quic .ApplicationErrorCode (ErrCodeGeneralProtocolError ), "" )
return nil , fmt .Errorf ("http3: failed to decode response headers: %w" , err )
}
res := s .response
if err := updateResponseFromHeaders (res , hfs ); err != nil {
s .CancelRead (quic .StreamErrorCode (ErrCodeMessageError ))
s .CancelWrite (quic .StreamErrorCode (ErrCodeMessageError ))
return nil , fmt .Errorf ("http3: invalid response: %w" , err )
}
respBody := newResponseBody (s .stream , 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
}
func (s *stream ) SendDatagram (b []byte ) error {
return s .datagrams .Send (b )
}
func (s *stream ) ReceiveDatagram (ctx context .Context ) ([]byte , error ) {
return s .datagrams .Receive (ctx )
}
type tracingReader struct {
io .Reader
first *bool
trace *httptrace .ClientTrace
}
func (r *tracingReader ) Read (b []byte ) (int , error ) {
n , err := r .Reader .Read (b )
if n > 0 && r .first != nil && !*r .first {
traceGotFirstResponseByte (r .trace )
*r .first = 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 .