123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- package h2mux
- import (
- "bytes"
- "encoding/binary"
- "fmt"
- "io"
- "net/url"
- "time"
- "github.com/rs/zerolog"
- "golang.org/x/net/http2"
- )
- const (
- CloudflaredProxyTunnelHostnameHeader = "cf-cloudflared-proxy-tunnel-hostname"
- )
- type MuxReader struct {
- // f is used to read HTTP2 frames.
- f *http2.Framer
- // handler provides a callback to receive new streams. if nil, new streams cannot be accepted.
- handler MuxedStreamHandler
- // streams tracks currently-open streams.
- streams *activeStreamMap
- // readyList is used to signal writable streams.
- readyList *ReadyList
- // streamErrors lets us report stream errors to the MuxWriter.
- streamErrors *StreamErrorMap
- // goAwayChan is used to tell the writer to send a GOAWAY message.
- goAwayChan chan<- http2.ErrCode
- // abortChan is used when shutting down ungracefully. When this becomes readable, all activity should stop.
- abortChan <-chan struct{}
- // pingTimestamp is an atomic value containing the latest received ping timestamp.
- pingTimestamp *PingTimestamp
- // connActive is used to signal to the writer that something happened on the connection.
- // This is used to clear idle timeout disconnection deadlines.
- connActive Signal
- // The initial value for the send and receive window of a new stream.
- initialStreamWindow uint32
- // The max value for the send window of a stream.
- streamWindowMax uint32
- // The max size for the write buffer of a stream
- streamWriteBufferMaxLen int
- // r is a reference to the underlying connection used when shutting down.
- r io.Closer
- // metricsUpdater is used to report metrics
- metricsUpdater muxMetricsUpdater
- // bytesRead is the amount of bytes read from data frames since the last time we called metricsUpdater.updateInBoundBytes()
- bytesRead *AtomicCounter
- // dictionaries holds the h2 cross-stream compression dictionaries
- dictionaries h2Dictionaries
- }
- // Shutdown blocks new streams from being created.
- // It returns a channel that is closed once the last stream has closed.
- func (r *MuxReader) Shutdown() <-chan struct{} {
- done, alreadyInProgress := r.streams.Shutdown()
- if alreadyInProgress {
- return done
- }
- r.sendGoAway(http2.ErrCodeNo)
- go func() {
- // close reader side when last stream ends; this will cause the writer to abort
- <-done
- r.r.Close()
- }()
- return done
- }
- func (r *MuxReader) run(log *zerolog.Logger) error {
- defer log.Debug().Msg("mux - read: event loop finished")
- // routine to periodically update bytesRead
- go func() {
- tickC := time.Tick(updateFreq)
- for {
- select {
- case <-r.abortChan:
- return
- case <-tickC:
- r.metricsUpdater.updateInBoundBytes(r.bytesRead.Count())
- }
- }
- }()
- for {
- frame, err := r.f.ReadFrame()
- if err != nil {
- errorString := fmt.Sprintf("mux - read: %s", err)
- if errorDetail := r.f.ErrorDetail(); errorDetail != nil {
- errorString = fmt.Sprintf("%s: errorDetail: %s", errorString, errorDetail)
- }
- switch e := err.(type) {
- case http2.StreamError:
- log.Info().Msgf("%s: stream error", errorString)
- // Ideally we wouldn't return here, since that aborts the muxer.
- // We should communicate the error to the relevant MuxedStream
- // data structure, so that callers of MuxedStream.Read() and
- // MuxedStream.Write() would see it. Then we could `continue`
- // and keep the muxer going.
- return r.streamError(e.StreamID, e.Code)
- case http2.ConnectionError:
- log.Info().Msgf("%s: stream error", errorString)
- return r.connectionError(err)
- default:
- if isConnectionClosedError(err) {
- if r.streams.Len() == 0 {
- // don't log the error here -- that would just be extra noise
- log.Debug().Msg("mux - read: shutting down")
- return nil
- }
- log.Info().Msgf("%s: connection closed unexpectedly", errorString)
- return err
- } else {
- log.Info().Msgf("%s: frame read error", errorString)
- return r.connectionError(err)
- }
- }
- }
- r.connActive.Signal()
- log.Debug().Msgf("mux - read: read frame: data %v", frame)
- switch f := frame.(type) {
- case *http2.DataFrame:
- err = r.receiveFrameData(f, log)
- case *http2.MetaHeadersFrame:
- err = r.receiveHeaderData(f)
- case *http2.RSTStreamFrame:
- streamID := f.Header().StreamID
- if streamID == 0 {
- return ErrInvalidStream
- }
- if stream, ok := r.streams.Get(streamID); ok {
- stream.Close()
- }
- r.streams.Delete(streamID)
- case *http2.PingFrame:
- r.receivePingData(f)
- case *http2.GoAwayFrame:
- err = r.receiveGoAway(f)
- // The receiver of a flow-controlled frame sends a WINDOW_UPDATE frame as it
- // consumes data and frees up space in flow-control windows
- case *http2.WindowUpdateFrame:
- err = r.updateStreamWindow(f)
- case *http2.UnknownFrame:
- switch f.Header().Type {
- case FrameUseDictionary:
- err = r.receiveUseDictionary(f)
- case FrameSetDictionary:
- err = r.receiveSetDictionary(f)
- default:
- err = ErrUnexpectedFrameType
- }
- default:
- err = ErrUnexpectedFrameType
- }
- if err != nil {
- log.Debug().Msgf("mux - read: read error: data %v", frame)
- return r.connectionError(err)
- }
- }
- }
- func (r *MuxReader) newMuxedStream(streamID uint32) *MuxedStream {
- return &MuxedStream{
- streamID: streamID,
- readBuffer: NewSharedBuffer(),
- writeBuffer: &bytes.Buffer{},
- writeBufferMaxLen: r.streamWriteBufferMaxLen,
- writeBufferHasSpace: make(chan struct{}, 1),
- receiveWindow: r.initialStreamWindow,
- receiveWindowCurrentMax: r.initialStreamWindow,
- receiveWindowMax: r.streamWindowMax,
- sendWindow: r.initialStreamWindow,
- readyList: r.readyList,
- dictionaries: r.dictionaries,
- }
- }
- // getStreamForFrame returns a stream if valid, or an error describing why the stream could not be returned.
- func (r *MuxReader) getStreamForFrame(frame http2.Frame) (*MuxedStream, error) {
- sid := frame.Header().StreamID
- if sid == 0 {
- return nil, ErrUnexpectedFrameType
- }
- if stream, ok := r.streams.Get(sid); ok {
- return stream, nil
- }
- if r.streams.IsLocalStreamID(sid) {
- // no stream available, but no error
- return nil, ErrClosedStream
- }
- if sid < r.streams.LastPeerStreamID() {
- // no stream available, stream closed error
- return nil, ErrClosedStream
- }
- return nil, ErrUnknownStream
- }
- func (r *MuxReader) defaultStreamErrorHandler(err error, header http2.FrameHeader) error {
- if header.Flags.Has(http2.FlagHeadersEndStream) {
- return nil
- } else if err == ErrUnknownStream || err == ErrClosedStream {
- return r.streamError(header.StreamID, http2.ErrCodeStreamClosed)
- } else {
- return err
- }
- }
- // Receives header frames from a stream. A non-nil error is a connection error.
- func (r *MuxReader) receiveHeaderData(frame *http2.MetaHeadersFrame) error {
- var stream *MuxedStream
- sid := frame.Header().StreamID
- if sid == 0 {
- return ErrUnexpectedFrameType
- }
- newStream := r.streams.IsPeerStreamID(sid)
- if newStream {
- // header request
- // TODO support trailers (if stream exists)
- ok, err := r.streams.AcquirePeerID(sid)
- if !ok {
- // ignore new streams while shutting down
- return r.streamError(sid, err)
- }
- stream = r.newMuxedStream(sid)
- // Set stream. Returns false if a stream already existed with that ID or we are shutting down, return false.
- if !r.streams.Set(stream) {
- // got HEADERS frame for an existing stream
- // TODO support trailers
- return r.streamError(sid, http2.ErrCodeInternal)
- }
- } else {
- // header response
- var err error
- if stream, err = r.getStreamForFrame(frame); err != nil {
- return r.defaultStreamErrorHandler(err, frame.Header())
- }
- }
- headers := make([]Header, 0, len(frame.Fields))
- for _, header := range frame.Fields {
- switch header.Name {
- case ":method":
- stream.method = header.Value
- case ":path":
- u, err := url.Parse(header.Value)
- if err == nil {
- stream.path = u.Path
- }
- case "accept-encoding":
- // remove accept-encoding if dictionaries are enabled
- if r.dictionaries.write != nil {
- continue
- }
- case CloudflaredProxyTunnelHostnameHeader:
- stream.tunnelHostname = TunnelHostname(header.Value)
- }
- headers = append(headers, Header{Name: header.Name, Value: header.Value})
- }
- stream.Headers = headers
- if frame.Header().Flags.Has(http2.FlagHeadersEndStream) {
- stream.receiveEOF()
- return nil
- }
- if newStream {
- go r.handleStream(stream)
- } else {
- close(stream.responseHeadersReceived)
- }
- return nil
- }
- func (r *MuxReader) handleStream(stream *MuxedStream) {
- defer stream.Close()
- r.handler.ServeStream(stream)
- }
- // Receives a data frame from a stream. A non-nil error is a connection error.
- func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, log *zerolog.Logger) error {
- stream, err := r.getStreamForFrame(frame)
- if err != nil {
- return r.defaultStreamErrorHandler(err, frame.Header())
- }
- data := frame.Data()
- if len(data) > 0 {
- n, err := stream.readBuffer.Write(data)
- if err != nil {
- return r.streamError(stream.streamID, http2.ErrCodeInternal)
- }
- r.bytesRead.IncrementBy(uint64(n))
- }
- if frame.Header().Flags.Has(http2.FlagDataEndStream) {
- if stream.receiveEOF() {
- r.streams.Delete(stream.streamID)
- log.Debug().Msgf("mux - read: stream closed: streamID: %d", frame.Header().StreamID)
- } else {
- log.Debug().Msgf("mux - read: shutdown receive side: streamID: %d", frame.Header().StreamID)
- }
- return nil
- }
- if !stream.consumeReceiveWindow(uint32(len(data))) {
- return r.streamError(stream.streamID, http2.ErrCodeFlowControl)
- }
- r.metricsUpdater.updateReceiveWindow(stream.getReceiveWindow())
- return nil
- }
- // Receive a PING from the peer. Update RTT and send/receive window metrics if it's an ACK.
- func (r *MuxReader) receivePingData(frame *http2.PingFrame) {
- ts := int64(binary.LittleEndian.Uint64(frame.Data[:]))
- if !frame.IsAck() {
- r.pingTimestamp.Set(ts)
- return
- }
- // Update the computed RTT aggregations with a new measurement.
- // `ts` is the time that the probe was sent.
- // We assume that `time.Now()` is the time we received that probe.
- r.metricsUpdater.updateRTT(&roundTripMeasurement{
- receiveTime: time.Now(),
- sendTime: time.Unix(0, ts),
- })
- }
- // Receive a GOAWAY from the peer. Gracefully shut down our connection.
- func (r *MuxReader) receiveGoAway(frame *http2.GoAwayFrame) error {
- r.Shutdown()
- // Close all streams above the last processed stream
- lastStream := r.streams.LastLocalStreamID()
- for i := frame.LastStreamID + 2; i <= lastStream; i++ {
- if stream, ok := r.streams.Get(i); ok {
- stream.Close()
- }
- }
- return nil
- }
- // Receive a USE_DICTIONARY from the peer. Setup dictionary for stream.
- func (r *MuxReader) receiveUseDictionary(frame *http2.UnknownFrame) error {
- payload := frame.Payload()
- streamID := frame.StreamID
- // Check frame is formatted properly
- if len(payload) != 1 {
- return r.streamError(streamID, http2.ErrCodeProtocol)
- }
- stream, err := r.getStreamForFrame(frame)
- if err != nil {
- return err
- }
- if stream.receivedUseDict == true || stream.dictionaries.read == nil {
- return r.streamError(streamID, http2.ErrCodeInternal)
- }
- stream.receivedUseDict = true
- dictID := payload[0]
- dictReader := stream.dictionaries.read.newReader(stream.readBuffer.(*SharedBuffer), dictID)
- if dictReader == nil {
- return r.streamError(streamID, http2.ErrCodeInternal)
- }
- stream.readBufferLock.Lock()
- stream.readBuffer = dictReader
- stream.readBufferLock.Unlock()
- return nil
- }
- // Receive a SET_DICTIONARY from the peer. Update dictionaries accordingly.
- func (r *MuxReader) receiveSetDictionary(frame *http2.UnknownFrame) (err error) {
- payload := frame.Payload()
- flags := frame.Flags
- stream, err := r.getStreamForFrame(frame)
- if err != nil && err != ErrClosedStream {
- return err
- }
- reader, ok := stream.readBuffer.(*h2DictionaryReader)
- if !ok {
- return r.streamError(frame.StreamID, http2.ErrCodeProtocol)
- }
- // A SetDictionary frame consists of several
- // Dictionary-Entries that specify how existing dictionaries
- // are to be updated using the current stream data
- // +---------------+---------------+
- // | Dictionary-Entry (+) ...
- // +---------------+---------------+
- for {
- // Each Dictionary-Entry is formatted as follows:
- // +-------------------------------+
- // | Dictionary-ID (8) |
- // +---+---------------------------+
- // | P | Size (7+) |
- // +---+---------------------------+
- // | E?| D?| Truncate? (6+) |
- // +---+---------------------------+
- // | Offset? (8+) |
- // +-------------------------------+
- var size, truncate, offset uint64
- var p, e, d bool
- // Parse a single Dictionary-Entry
- if len(payload) < 2 { // Must have at least id and size
- return MuxerStreamError{"unexpected EOF", http2.ErrCodeProtocol}
- }
- dictID := uint8(payload[0])
- p = (uint8(payload[1]) >> 7) == 1
- payload, size, err = http2ReadVarInt(7, payload[1:])
- if err != nil {
- return
- }
- if flags.Has(FlagSetDictionaryAppend) {
- // Presence of FlagSetDictionaryAppend means we expect e, d and truncate
- if len(payload) < 1 {
- return MuxerStreamError{"unexpected EOF", http2.ErrCodeProtocol}
- }
- e = (uint8(payload[0]) >> 7) == 1
- d = (uint8((payload[0])>>6) & 1) == 1
- payload, truncate, err = http2ReadVarInt(6, payload)
- if err != nil {
- return
- }
- }
- if flags.Has(FlagSetDictionaryOffset) {
- // Presence of FlagSetDictionaryOffset means we expect offset
- if len(payload) < 1 {
- return MuxerStreamError{"unexpected EOF", http2.ErrCodeProtocol}
- }
- payload, offset, err = http2ReadVarInt(8, payload)
- if err != nil {
- return
- }
- }
- setdict := setDictRequest{streamID: stream.streamID,
- dictID: dictID,
- dictSZ: size,
- truncate: truncate,
- offset: offset,
- P: p,
- E: e,
- D: d}
- // Find the right dictionary
- dict, err := r.dictionaries.read.getDictByID(dictID)
- if err != nil {
- return err
- }
- // Register a dictionary update order for the dictionary and reader
- updateEntry := &dictUpdate{reader: reader, dictionary: dict, s: setdict}
- dict.queue = append(dict.queue, updateEntry)
- reader.queue = append(reader.queue, updateEntry)
- // End of frame
- if len(payload) == 0 {
- break
- }
- }
- return nil
- }
- // Receives header frames from a stream. A non-nil error is a connection error.
- func (r *MuxReader) updateStreamWindow(frame *http2.WindowUpdateFrame) error {
- stream, err := r.getStreamForFrame(frame)
- if err != nil && err != ErrUnknownStream && err != ErrClosedStream {
- return err
- }
- if stream == nil {
- // ignore window updates on closed streams
- return nil
- }
- stream.replenishSendWindow(frame.Increment)
- r.metricsUpdater.updateSendWindow(stream.getSendWindow())
- return nil
- }
- // Raise a stream processing error, closing the stream. Runs on the write thread.
- func (r *MuxReader) streamError(streamID uint32, e http2.ErrCode) error {
- r.streamErrors.RaiseError(streamID, e)
- return nil
- }
- func (r *MuxReader) connectionError(err error) error {
- http2Code := http2.ErrCodeInternal
- switch e := err.(type) {
- case http2.ConnectionError:
- http2Code = http2.ErrCode(e)
- case MuxerProtocolError:
- http2Code = e.h2code
- }
- r.sendGoAway(http2Code)
- return err
- }
- // Instruct the writer to send a GOAWAY message if possible. This may fail in
- // the case where an existing GOAWAY message is in flight or the writer event
- // loop already ended.
- func (r *MuxReader) sendGoAway(errCode http2.ErrCode) {
- select {
- case r.goAwayChan <- errCode:
- default:
- }
- }
|