mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-05-10 16:16:36 +00:00

time.Tick() does not get garbage collected because the channel underneath never gets deleted and the underlying Ticker can never be recovered by the garbage collector. We replace this with NewTicker() to avoid this.
312 lines
9.8 KiB
Go
312 lines
9.8 KiB
Go
package h2mux
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"golang.org/x/net/http2"
|
|
"golang.org/x/net/http2/hpack"
|
|
)
|
|
|
|
type MuxWriter struct {
|
|
// f is used to write HTTP2 frames.
|
|
f *http2.Framer
|
|
// streams tracks currently-open streams.
|
|
streams *activeStreamMap
|
|
// streamErrors receives stream errors raised by the MuxReader.
|
|
streamErrors *StreamErrorMap
|
|
// readyStreamChan is used to multiplex writable streams onto the single connection.
|
|
// When a stream becomes writable its ID is sent on this channel.
|
|
readyStreamChan <-chan uint32
|
|
// newStreamChan is used to create new streams with a given set of headers.
|
|
newStreamChan <-chan MuxedStreamRequest
|
|
// goAwayChan is used to send a single GOAWAY message to the peer. The element received
|
|
// is the HTTP/2 error code to send.
|
|
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
|
|
// A timer used to measure idle connection time. Reset after sending data.
|
|
idleTimer *IdleTimer
|
|
// connActiveChan receives a signal that the connection received some (read) activity.
|
|
connActiveChan <-chan struct{}
|
|
// Maximum size of all frames that can be sent on this connection.
|
|
maxFrameSize uint32
|
|
// headerEncoder is the stateful header encoder for this connection
|
|
headerEncoder *hpack.Encoder
|
|
// headerBuffer is the temporary buffer used by headerEncoder.
|
|
headerBuffer bytes.Buffer
|
|
|
|
// metricsUpdater is used to report metrics
|
|
metricsUpdater muxMetricsUpdater
|
|
// bytesWrote is the amount of bytes written to data frames since the last time we called metricsUpdater.updateOutBoundBytes()
|
|
bytesWrote *AtomicCounter
|
|
|
|
useDictChan <-chan useDictRequest
|
|
}
|
|
|
|
type MuxedStreamRequest struct {
|
|
stream *MuxedStream
|
|
body io.Reader
|
|
}
|
|
|
|
func NewMuxedStreamRequest(stream *MuxedStream, body io.Reader) MuxedStreamRequest {
|
|
return MuxedStreamRequest{
|
|
stream: stream,
|
|
body: body,
|
|
}
|
|
}
|
|
|
|
func (r *MuxedStreamRequest) flushBody() {
|
|
io.Copy(r.stream, r.body)
|
|
r.stream.CloseWrite()
|
|
}
|
|
|
|
func tsToPingData(ts int64) [8]byte {
|
|
pingData := [8]byte{}
|
|
binary.LittleEndian.PutUint64(pingData[:], uint64(ts))
|
|
return pingData
|
|
}
|
|
|
|
func (w *MuxWriter) run(log *zerolog.Logger) error {
|
|
defer log.Debug().Msg("mux - write: event loop finished")
|
|
|
|
// routine to periodically communicate bytesWrote
|
|
go func() {
|
|
ticker := time.NewTicker(updateFreq)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-w.abortChan:
|
|
return
|
|
case <-ticker.C:
|
|
w.metricsUpdater.updateOutBoundBytes(w.bytesWrote.Count())
|
|
}
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-w.abortChan:
|
|
log.Debug().Msg("mux - write: aborting writer thread")
|
|
return nil
|
|
case errCode := <-w.goAwayChan:
|
|
log.Debug().Msgf("mux - write: sending GOAWAY code %v", errCode)
|
|
err := w.f.WriteGoAway(w.streams.LastPeerStreamID(), errCode, []byte{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.idleTimer.MarkActive()
|
|
case <-w.pingTimestamp.GetUpdateChan():
|
|
log.Debug().Msg("mux - write: sending PING ACK")
|
|
err := w.f.WritePing(true, tsToPingData(w.pingTimestamp.Get()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.idleTimer.MarkActive()
|
|
case <-w.idleTimer.C:
|
|
if !w.idleTimer.Retry() {
|
|
return ErrConnectionDropped
|
|
}
|
|
log.Debug().Msg("mux - write: sending PING")
|
|
err := w.f.WritePing(false, tsToPingData(time.Now().UnixNano()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.idleTimer.ResetTimer()
|
|
case <-w.connActiveChan:
|
|
w.idleTimer.MarkActive()
|
|
case <-w.streamErrors.GetSignalChan():
|
|
for streamID, errCode := range w.streamErrors.GetErrors() {
|
|
log.Debug().Msgf("mux - write: resetting stream with code: %v streamID: %d", errCode, streamID)
|
|
err := w.f.WriteRSTStream(streamID, errCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
w.idleTimer.MarkActive()
|
|
case streamRequest := <-w.newStreamChan:
|
|
streamID := w.streams.AcquireLocalID()
|
|
streamRequest.stream.streamID = streamID
|
|
if !w.streams.Set(streamRequest.stream) {
|
|
// Race between OpenStream and Shutdown, and Shutdown won. Let Shutdown (and the eventual abort) take
|
|
// care of this stream. Ideally we'd pass the error directly to the stream object somehow so the
|
|
// caller can be unblocked sooner, but the value of that optimisation is minimal for most of the
|
|
// reasons why you'd call Shutdown anyway.
|
|
continue
|
|
}
|
|
if streamRequest.body != nil {
|
|
go streamRequest.flushBody()
|
|
}
|
|
err := w.writeStreamData(streamRequest.stream, log)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.idleTimer.MarkActive()
|
|
case streamID := <-w.readyStreamChan:
|
|
stream, ok := w.streams.Get(streamID)
|
|
if !ok {
|
|
continue
|
|
}
|
|
err := w.writeStreamData(stream, log)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.idleTimer.MarkActive()
|
|
case useDict := <-w.useDictChan:
|
|
err := w.writeUseDictionary(useDict)
|
|
if err != nil {
|
|
log.Error().Msgf("mux - write: error writing use dictionary: %s", err)
|
|
return err
|
|
}
|
|
w.idleTimer.MarkActive()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *MuxWriter) writeStreamData(stream *MuxedStream, log *zerolog.Logger) error {
|
|
log.Debug().Msgf("mux - write: writable: streamID: %d", stream.streamID)
|
|
chunk := stream.getChunk()
|
|
w.metricsUpdater.updateReceiveWindow(stream.getReceiveWindow())
|
|
w.metricsUpdater.updateSendWindow(stream.getSendWindow())
|
|
if chunk.sendHeadersFrame() {
|
|
err := w.writeHeaders(chunk.streamID, chunk.headers)
|
|
if err != nil {
|
|
log.Error().Msgf("mux - write: error writing headers: %s: streamID: %d", err, stream.streamID)
|
|
return err
|
|
}
|
|
log.Debug().Msgf("mux - write: output headers: streamID: %d", stream.streamID)
|
|
}
|
|
|
|
if chunk.sendWindowUpdateFrame() {
|
|
// Send a WINDOW_UPDATE frame to update our receive window.
|
|
// If the Stream ID is zero, the window update applies to the connection as a whole
|
|
// RFC7540 section-6.9.1 "A receiver that receives a flow-controlled frame MUST
|
|
// always account for its contribution against the connection flow-control
|
|
// window, unless the receiver treats this as a connection error"
|
|
err := w.f.WriteWindowUpdate(chunk.streamID, chunk.windowUpdate)
|
|
if err != nil {
|
|
log.Error().Msgf("mux - write: error writing window update: %s: streamID: %d", err, stream.streamID)
|
|
return err
|
|
}
|
|
log.Debug().Msgf("mux - write: increment receive window by %d streamID: %d", chunk.windowUpdate, stream.streamID)
|
|
}
|
|
|
|
for chunk.sendDataFrame() {
|
|
payload, sentEOF := chunk.nextDataFrame(int(w.maxFrameSize))
|
|
err := w.f.WriteData(chunk.streamID, sentEOF, payload)
|
|
if err != nil {
|
|
log.Error().Msgf("mux - write: error writing data: %s: streamID: %d", err, stream.streamID)
|
|
return err
|
|
}
|
|
// update the amount of data wrote
|
|
w.bytesWrote.IncrementBy(uint64(len(payload)))
|
|
log.Debug().Msgf("mux - write: output data: %d: streamID: %d", len(payload), stream.streamID)
|
|
|
|
if sentEOF {
|
|
if stream.readBuffer.Closed() {
|
|
// transition into closed state
|
|
if !stream.gotReceiveEOF() {
|
|
// the peer may send data that we no longer want to receive. Force them into the
|
|
// closed state.
|
|
log.Debug().Msgf("mux - write: resetting stream: streamID: %d", stream.streamID)
|
|
w.f.WriteRSTStream(chunk.streamID, http2.ErrCodeNo)
|
|
} else {
|
|
// Half-open stream transitioned into closed
|
|
log.Debug().Msgf("mux - write: closing stream: streamID: %d", stream.streamID)
|
|
}
|
|
w.streams.Delete(chunk.streamID)
|
|
} else {
|
|
log.Debug().Msgf("mux - write: closing stream write side: streamID: %d", stream.streamID)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *MuxWriter) encodeHeaders(headers []Header) ([]byte, error) {
|
|
w.headerBuffer.Reset()
|
|
for _, header := range headers {
|
|
err := w.headerEncoder.WriteField(hpack.HeaderField{
|
|
Name: header.Name,
|
|
Value: header.Value,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return w.headerBuffer.Bytes(), nil
|
|
}
|
|
|
|
// writeHeaders writes a block of encoded headers, splitting it into multiple frames if necessary.
|
|
func (w *MuxWriter) writeHeaders(streamID uint32, headers []Header) error {
|
|
encodedHeaders, err := w.encodeHeaders(headers)
|
|
if err != nil || len(encodedHeaders) == 0 {
|
|
return err
|
|
}
|
|
|
|
blockSize := int(w.maxFrameSize)
|
|
// CONTINUATION is unnecessary; the headers fit within the blockSize
|
|
if len(encodedHeaders) < blockSize {
|
|
return w.f.WriteHeaders(http2.HeadersFrameParam{
|
|
StreamID: streamID,
|
|
EndHeaders: true,
|
|
BlockFragment: encodedHeaders,
|
|
})
|
|
}
|
|
|
|
choppedHeaders := chopEncodedHeaders(encodedHeaders, blockSize)
|
|
// len(choppedHeaders) is at least 2
|
|
if err := w.f.WriteHeaders(http2.HeadersFrameParam{StreamID: streamID, EndHeaders: false, BlockFragment: choppedHeaders[0]}); err != nil {
|
|
return err
|
|
}
|
|
for i := 1; i < len(choppedHeaders)-1; i++ {
|
|
if err := w.f.WriteContinuation(streamID, false, choppedHeaders[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := w.f.WriteContinuation(streamID, true, choppedHeaders[len(choppedHeaders)-1]); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Partition a slice of bytes into `len(slice) / blockSize` slices of length `blockSize`
|
|
func chopEncodedHeaders(headers []byte, chunkSize int) [][]byte {
|
|
var divided [][]byte
|
|
|
|
for i := 0; i < len(headers); i += chunkSize {
|
|
end := i + chunkSize
|
|
|
|
if end > len(headers) {
|
|
end = len(headers)
|
|
}
|
|
|
|
divided = append(divided, headers[i:end])
|
|
}
|
|
|
|
return divided
|
|
}
|
|
|
|
func (w *MuxWriter) writeUseDictionary(dictRequest useDictRequest) error {
|
|
err := w.f.WriteRawFrame(FrameUseDictionary, 0, dictRequest.streamID, []byte{byte(dictRequest.dictID)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload := make([]byte, 0, 64)
|
|
for _, set := range dictRequest.setDict {
|
|
payload = append(payload, byte(set.dictID))
|
|
payload = appendVarInt(payload, 7, uint64(set.dictSZ))
|
|
payload = append(payload, 0x80) // E = 1, D = 0, Truncate = 0
|
|
}
|
|
|
|
err = w.f.WriteRawFrame(FrameSetDictionary, FlagSetDictionaryAppend, dictRequest.streamID, payload)
|
|
return err
|
|
}
|