Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http2: RFC8441 extended connect protocol #1

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions http2/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -1478,15 +1478,15 @@ 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
default:
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 {
Expand Down
31 changes: 19 additions & 12 deletions http2/http2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions http2/http2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
81 changes: 62 additions & 19 deletions http2/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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++
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
50 changes: 37 additions & 13 deletions http2/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find a good way to plumb down the protocol field, maybe adding a new semantic to the current protocol field? it seems is unused right now for clients

	// The protocol version for incoming server requests.
	//
	// For client requests, these fields are ignored. The HTTP
	// client code always uses either HTTP/1.1 or HTTP/2.
	// See the docs on Transport for details.
	Proto      string // "HTTP/1.0"


}
}
if trailers != "" {
f("trailer", trailers)
Expand Down Expand Up @@ -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)
Expand Down