diff --git a/http2/frame.go b/http2/frame.go index 96a747905..86c5bb2d5 100644 --- a/http2/frame.go +++ b/http2/frame.go @@ -1478,7 +1478,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error { pf := mh.PseudoFields() for i, hf := range pf { switch hf.Name { - case ":method", ":path", ":scheme", ":authority": + case ":method", ":path", ":scheme", ":authority", ":protocol": isRequest = true case ":status": isResponse = true @@ -1486,7 +1486,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error { return pseudoHeaderError(hf.Name) } // Check for duplicates. - // This would be a bad algorithm, but N is 4. + // This would be a bad algorithm, but N is 5. // And this doesn't allocate. for _, hf2 := range pf[:i] { if hf.Name == hf2.Name { diff --git a/http2/http2.go b/http2/http2.go index 5571ccfd2..c5bfe536e 100644 --- a/http2/http2.go +++ b/http2/http2.go @@ -139,6 +139,11 @@ func (s Setting) Valid() error { if s.Val < 16384 || s.Val > 1<<24-1 { return ConnectionError(ErrCodeProtocol) } + // https://datatracker.ietf.org/doc/html/rfc8441#section-3 + case SettingEnableConnectProtocol: + if s.Val != 1 && s.Val != 0 { + return ConnectionError(ErrCodeProtocol) + } } return nil } @@ -148,21 +153,23 @@ func (s Setting) Valid() error { type SettingID uint16 const ( - SettingHeaderTableSize SettingID = 0x1 - SettingEnablePush SettingID = 0x2 - SettingMaxConcurrentStreams SettingID = 0x3 - SettingInitialWindowSize SettingID = 0x4 - SettingMaxFrameSize SettingID = 0x5 - SettingMaxHeaderListSize SettingID = 0x6 + SettingHeaderTableSize SettingID = 0x1 + SettingEnablePush SettingID = 0x2 + SettingMaxConcurrentStreams SettingID = 0x3 + SettingInitialWindowSize SettingID = 0x4 + SettingMaxFrameSize SettingID = 0x5 + SettingMaxHeaderListSize SettingID = 0x6 + SettingEnableConnectProtocol SettingID = 0x8 // RFC 8441 ) var settingName = map[SettingID]string{ - SettingHeaderTableSize: "HEADER_TABLE_SIZE", - SettingEnablePush: "ENABLE_PUSH", - SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS", - SettingInitialWindowSize: "INITIAL_WINDOW_SIZE", - SettingMaxFrameSize: "MAX_FRAME_SIZE", - SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE", + SettingHeaderTableSize: "HEADER_TABLE_SIZE", + SettingEnablePush: "ENABLE_PUSH", + SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS", + SettingInitialWindowSize: "INITIAL_WINDOW_SIZE", + SettingMaxFrameSize: "MAX_FRAME_SIZE", + SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE", + SettingEnableConnectProtocol: "ENABLE_CONNECT_PROTOCOL", } func (s SettingID) String() string { diff --git a/http2/http2_test.go b/http2/http2_test.go index f77c08a10..6e646ddbe 100644 --- a/http2/http2_test.go +++ b/http2/http2_test.go @@ -44,6 +44,7 @@ func TestSettingString(t *testing.T) { }{ {Setting{SettingMaxFrameSize, 123}, "[MAX_FRAME_SIZE = 123]"}, {Setting{1<<16 - 1, 123}, "[UNKNOWN_SETTING_65535 = 123]"}, + {Setting{SettingEnableConnectProtocol, 1}, "[ENABLE_CONNECT_PROTOCOL = 1]"}, } for i, tt := range tests { got := fmt.Sprint(tt.s) diff --git a/http2/server.go b/http2/server.go index e644d9b2f..72a843d88 100644 --- a/http2/server.go +++ b/http2/server.go @@ -136,6 +136,10 @@ type Server struct { // The errType consists of only ASCII word characters. CountError func(errType string) + // EnableConnectProtocol, if true, allows clients to use the Extended + // Connect Protocol as defined by RFC 8441. + EnableConnectProtocol bool + // Internal state. This is a pointer (rather than embedded directly) // so that we don't embed a Mutex in this struct, which will make the // struct non-copyable, which might break some callers. @@ -170,6 +174,13 @@ func (s *Server) maxConcurrentStreams() uint32 { return defaultMaxStreams } +func (s *Server) enableConnectProtocol() uint32 { + if s.EnableConnectProtocol { + return 1 + } + return 0 +} + // maxQueuedControlFrames is the maximum number of control frames like // SETTINGS, PING and RST_STREAM that will be queued for writing before // the connection is closed to prevent memory exhaustion attacks. @@ -383,6 +394,7 @@ func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) { headerTableSize: initialHeaderTableSize, serveG: newGoroutineLock(), pushEnabled: true, + connectProtocolEnabled: s.enableConnectProtocol(), } s.state.registerConn(sc) @@ -490,24 +502,25 @@ func (sc *serverConn) rejectConn(err ErrCode, debug string) { type serverConn struct { // Immutable: - srv *Server - hs *http.Server - conn net.Conn - bw *bufferedWriter // writing to conn - handler http.Handler - baseCtx context.Context - framer *Framer - doneServing chan struct{} // closed when serverConn.serve ends - readFrameCh chan readFrameResult // written by serverConn.readFrames - wantWriteFrameCh chan FrameWriteRequest // from handlers -> serve - wroteFrameCh chan frameWriteResult // from writeFrameAsync -> serve, tickles more frame writes - bodyReadCh chan bodyReadMsg // from handlers -> serve - serveMsgCh chan interface{} // misc messages & code to send to / run on the serve loop - flow flow // conn-wide (not stream-specific) outbound flow control - inflow flow // conn-wide inbound flow control - tlsState *tls.ConnectionState // shared by all handlers, like net/http - remoteAddrStr string - writeSched WriteScheduler + srv *Server + hs *http.Server + conn net.Conn + bw *bufferedWriter // writing to conn + handler http.Handler + baseCtx context.Context + framer *Framer + doneServing chan struct{} // closed when serverConn.serve ends + readFrameCh chan readFrameResult // written by serverConn.readFrames + wantWriteFrameCh chan FrameWriteRequest // from handlers -> serve + wroteFrameCh chan frameWriteResult // from writeFrameAsync -> serve, tickles more frame writes + bodyReadCh chan bodyReadMsg // from handlers -> serve + serveMsgCh chan interface{} // misc messages & code to send to / run on the serve loop + flow flow // conn-wide (not stream-specific) outbound flow control + inflow flow // conn-wide inbound flow control + tlsState *tls.ConnectionState // shared by all handlers, like net/http + connectProtocolEnabled uint32 // 0: false 1: true + remoteAddrStr string + writeSched WriteScheduler // Everything following is owned by the serve loop; use serveG.check(): serveG goroutineLock // used to verify funcs are on serve() @@ -829,6 +842,7 @@ func (sc *serverConn) serve() { {SettingMaxConcurrentStreams, sc.advMaxStreams}, {SettingMaxHeaderListSize, sc.maxHeaderListSize()}, {SettingInitialWindowSize, uint32(sc.srv.initialStreamRecvWindowSize())}, + {SettingEnableConnectProtocol, sc.srv.enableConnectProtocol()}, }, }) sc.unackedSettings++ @@ -2012,9 +2026,29 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res scheme: f.PseudoValue("scheme"), authority: f.PseudoValue("authority"), path: f.PseudoValue("path"), + protocol: f.PseudoValue("protocol"), } isConnect := rp.method == "CONNECT" + isExtendedConnect := rp.protocol != "" + // Extended Connect Protocol RFC 8441 + if isExtendedConnect { + // If a client were to use the provisions of the extended CONNECT + // method without first receiving a SETTINGS_ENABLE_CONNECT_PROTOCOL + // parameter, a non-supporting peer would detect a malformed + // request and generate a stream error. + if !sc.srv.EnableConnectProtocol { + return nil, nil, sc.countError("bad_protocol_method", streamError(f.StreamID, ErrCodeProtocol)) + } + // :protocol contains a value from "Hypertext Transfer Protocol (HTTP) Upgrade Token Registry" + // https://www.iana.org/assignments/http-upgrade-tokens/http-upgrade-tokens.xhtml + switch rp.protocol { + case "TLS", "HTTP", "WebSocket", "websocket", "h2c": + default: + return nil, nil, sc.countError("bad_protocol_method", streamError(f.StreamID, ErrCodeProtocol)) + } + } + if isConnect { if rp.path != "" || rp.scheme != "" || rp.authority == "" { return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol)) @@ -2071,6 +2105,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res type requestParam struct { method string scheme, authority, path string + protocol string header http.Header } @@ -2112,9 +2147,17 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*r var url_ *url.URL var requestURI string + protocol := "HTTP/2.0" if rp.method == "CONNECT" { + // On requests bearing the :protocol pseudo-header field, the + // :authority pseudo-header field is interpreted according to + // Section 8.1.2.3 of [RFC7540] instead of Section 8.3 of that + // document. url_ = &url.URL{Host: rp.authority} requestURI = rp.authority // mimic HTTP/1 server behavior + if rp.protocol != "" { + protocol = rp.protocol + } } else { var err error url_, err = url.ParseRequestURI(rp.path) @@ -2135,7 +2178,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*r RemoteAddr: sc.remoteAddrStr, Header: rp.header, RequestURI: requestURI, - Proto: "HTTP/2.0", + Proto: protocol, ProtoMajor: 2, ProtoMinor: 0, TLS: tlsState, diff --git a/http2/transport.go b/http2/transport.go index f135b0f75..81588c276 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -282,10 +282,11 @@ type ClientConn struct { lastActive time.Time lastIdle time.Time // time last idle // Settings from peer: (also guarded by wmu) - maxFrameSize uint32 - maxConcurrentStreams uint32 - peerMaxHeaderListSize uint64 - initialWindowSize uint32 + maxFrameSize uint32 + maxConcurrentStreams uint32 + peerMaxHeaderListSize uint64 + initialWindowSize uint32 + connectProtocolEnabled bool // reqHeaderMu is a 1-element semaphore channel controlling access to sending new requests. // Write to reqHeaderMu to lock it, read from it to unlock. @@ -824,6 +825,10 @@ type ClientConnState struct { // LastIdle, if non-zero, is when the connection last // transitioned to idle state. LastIdle time.Time + + // ExtendedConnectProtocol reports whether the extended connect protocol + // is supported in the connection. + ExtendedConnectProtocol bool } // State returns a snapshot of cc's state. @@ -838,13 +843,14 @@ func (cc *ClientConn) State() ClientConnState { cc.mu.Lock() defer cc.mu.Unlock() return ClientConnState{ - Closed: cc.closed, - Closing: cc.closing || cc.singleUse || cc.doNotReuse || cc.goAway != nil, - StreamsActive: len(cc.streams), - StreamsReserved: cc.streamsReserved, - StreamsPending: cc.pendingRequests, - LastIdle: cc.lastIdle, - MaxConcurrentStreams: maxConcurrent, + Closed: cc.closed, + Closing: cc.closing || cc.singleUse || cc.doNotReuse || cc.goAway != nil, + StreamsActive: len(cc.streams), + StreamsReserved: cc.streamsReserved, + StreamsPending: cc.pendingRequests, + LastIdle: cc.lastIdle, + MaxConcurrentStreams: maxConcurrent, + ExtendedConnectProtocol: cc.connectProtocolEnabled, } } @@ -1724,7 +1730,10 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail } var path string - if req.Method != "CONNECT" { + // On requests that contain the :protocol pseudo-header field, the + // :scheme and :path pseudo-header fields of the target URI MUST also be included. + isExtendedConnect := req.Method == "CONNECT" && cc.connectProtocolEnabled && req.Proto != "" + if req.Method != "CONNECT" || isExtendedConnect { path = req.URL.RequestURI() if !validPseudoPath(path) { orig := path @@ -1765,9 +1774,13 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail m = http.MethodGet } f(":method", m) - if req.Method != "CONNECT" { + if req.Method != "CONNECT" || isExtendedConnect { f(":path", path) f(":scheme", req.URL.Scheme) + if isExtendedConnect { + f(":protocol", req.Proto) + + } } if trailers != "" { f("trailer", trailers) @@ -2706,6 +2719,17 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error { cc.cond.Broadcast() cc.initialWindowSize = s.Val + case SettingEnableConnectProtocol: + // A sender MUST NOT send the parameter with the value 0 + // after previously sending a value 1. + if cc.connectProtocolEnabled && s.Val == 0 { + return ConnectionError(ErrCodeProtocol) + } + if s.Val == 1 { + cc.connectProtocolEnabled = true + } else if s.Val > 1 { + return ConnectionError(ErrCodeProtocol) + } default: // TODO(bradfitz): handle more settings? SETTINGS_HEADER_TABLE_SIZE probably. cc.vlogf("Unhandled Setting: %v", s)