Skip to content

proposal: x/net/http2: support for WebSockets over HTTP/2 #49918

Open
@mitar

Description

@mitar

What version of Go are you using (go version)?

go version go1.17.3 linux/amd64

Does this issue reproduce with the latest release?

Yes.

What did you do?

I wanted to use Websockets over HTTP2.

What did you expect to see?

Support for it in Go standard library.

What did you see instead?

It does not support it yet.

See #46319 for background.

WebSockets over HTTP2 have been standardized as RFC 8441 and Firefox and Chromium supports that. Thus, I suggest that support is added for Websockets over HTTP2. The server should also be able to specify using HTTP/2 SETTINGS parameter that it supports Websockets over HTTP2.

Activity

added this to the Proposal milestone on Dec 2, 2021
aojea

aojea commented on Dec 21, 2021

@aojea
Contributor

also related #27244

aojea

aojea commented on Dec 21, 2021

@aojea
Contributor

Just for reference https://datatracker.ietf.org/doc/html/rfc8441 it seems this requires (please correct me if I'm wrong, not very familiar with the stdlib implemententation):

  1. Implement the SETTINGS_ENABLE_CONNECT_PROTOCOL SETTINGS

Requires adding an HTTP2 server option to enable the setting

  1. Implement the The Extended CONNECT Method

This requires adding a new field to the requests

   o  A new pseudo-header field :protocol MAY be included on request
      HEADERS indicating the desired protocol to be spoken on the tunnel
      created by CONNECT.  The pseudo-header field is single valued and
      contains a value from the "Hypertext Transfer Protocol (HTTP)
      Upgrade Token Registry" located at
      <https://www.iana.org/assignments/http-upgrade-tokens/>

and adding the logic to use the field if SETTINGS_ENABLE_CONNECT_PROTOCOL has been received.

EDIT1 :/

go/src/net/http/request.go

Lines 107 to 109 in 90fb5a4

// Go's HTTP client does not support sending a request with
// the CONNECT method. See the documentation on Transport for
// details.

EDIT2, so this seems to be on purpose

#22554 (comment)

So most users of CONNECT (including Go's build infrastructure in: https://github.com/golang/build/blob/ce623a5/cmd/buildlet/reverse.go#L213) either fmt.Fprintf the CONNECT request themselves, or do *http.Request.Write it to a self-dialed net.Conn, rather than using *http.Transport.

  1. Bootstrap the websocket protocol

HTTP/2 WebSockets need a way to read from and write to the HTTP/2 stream for a request, not a way to hijack the connection.

aojea

aojea commented on Dec 22, 2021

@aojea
Contributor

I think that this will only require adding a new option on the http2 server to enable the SETTINGS_ENABLE_CONNECT_PROTOCOL option and another one in the client to let the user know if that setting is enable.

If I understand correctly how CONNECT works in golang, the rest of the logic can be done at a higher level in a similar way that is being done with https://github.com/golang/net/tree/master/websocket

JeremyLoy

JeremyLoy commented on Jan 7, 2022

@JeremyLoy

Considering the gorilla/websocket library is currently unmaintained, which is causing a lot of concern in the community, I think this is a great opportunity to solidify and complete websocket support in the standard library

aojea

aojea commented on Jan 9, 2022

@aojea
Contributor

I did a prototype https://github.com/aojea/net/pull/1/files to get a better idea on what will be required to implement this.

My main concern is that golang seems to avoid implementing the CONNECT semantics in the stdlib #22554 (comment), leaving the implementation to the users.
The requirement of the Extended CONNECT Method will require to add new CONNECT semantics (including a new pseudoheader) to the stdlib, I don't know what the golang team think about this 😄

hexfusion

hexfusion commented on Feb 21, 2022

@hexfusion

How can we move this forward? I think many folks are interested in the implementation of rfc8441 it has been a few years[1].

@neild could you possibly help direct us on next steps?

[1] #32763

ethanpailes

ethanpailes commented on May 26, 2022

@ethanpailes

I've started poking at this a bit and it seems that a resolution will require two patches: one to the x/net/http2 package to allow stream oriented interaction with an http2 stream, and then a followup change to x/net/websocket to add the actual websocket support. I've talked about this a bit on the go-nuts mailing list, but it really should be a public discussion, so I'm moving it here. Here's the most recent message I posted about a strawman patch I've put together:

I've got something working for exposing HTTP/2 streams as Stream struct that is spiritually an io.ReadWriteCloser on the client side, and using existing interfaces on the server side. I'm posting about it here to give people a chance to share initial thoughts before I write up something more formal. I've posted my changes on gerrit in case anyone wants to look at the details.

On the server side of things, it seems that the current interface is basically already adequate except for the fact that the server currently rejects any requests using non https/http schemes. The http.ResponseWriter that gets passed down into HTTP/2 handlers implements http.Flusher, allowing you to force writes out on to the wire, and you can just use the http.Request.Body as the reader for the stream. The right thing to do here seems relatively straightforward. We can do one of a few things:

  1. Convert the scheme check to a regex check based on RFC3986 3.1. The spec does say in 8.1.2.3 that "All HTTP/2 requests MUST include exactly one valid value for the ":method", ":scheme", and ":path" pseudo-header fields, ... An HTTP request that omits mandatory pseudo-header fields is malformed (Section 8.1.2.6)." (emphasis mine), but in the same section it also says '":scheme" is not restricted to "http" and "https" schemed URIs.'. I think this should be read to mean that the only check we should be performing on scheme is that it parses correctly according to RFC3986 3.1. This is the option I think makes the most sense.
  2. Just expand the allowlist in the check so that it includes wss and ws as well as http and https. This is a more conservative option and I think can be defended on the basis that the http library isn't generally expected to serve as a proxy, so a fixed list of allowed schemes is fine.
  3. Drop the check entirely as I've done in my strawman patch. This seems wrong.

In terms of the client side of things, I'm relatively happy with the interface for the Stream struct I added. It re-uses the http.Request and http.Response types to let people set and read headers in a familiar way, and is basically just an unpacked version of the RoundTrip routine that keeps the request open after the stream gets initially created and only closes it when the user explicitly asks for a close. There are some things that seem clunky about it to me though:

  1. It's hanging off the http2.Transport struct. As a user I would expect something like this on the http.Client, so maybe it is better to put it there somehow. That does run into the issue of this routine not being available for HTTP/1.1 though.
  2. It might be better not to re-use the http.Request/http.Response types since we aren't really making requests and responses. Maybe a more unpacked API that directly references headers and trailers makes more sense.
  3. I always feel weird about performing IO operations without passing a context down. Currently Stream uses the context attached to the request passed in to open the stream, which seems fine, but maybe a more explicit API would be better.

cc: @neild @ianlancetaylor

ethanpailes

ethanpailes commented on May 27, 2022

@ethanpailes

I just noticed https://datatracker.ietf.org/doc/html/rfc8441#section-5 saying that for HTTP/2 wss:// URIs should be translated to https:// and ws:// URIs should be translated to http:// URIs, so it seems that we may not need to make any changes to the server side of the HTTP/2 stack after all.

neild

neild commented on May 27, 2022

@neild
Contributor

Commented on https://go.dev/cl/408835: Do we need a new stream-oriented Transport API? RoundTrip already keeps the request stream open so long as the request or response body are still being written.

We'd need to add support for SETTINGS_ENABLE_CONNECT_PROTOCOL. That should be straightforward.

We also need a way to set/get the :protocol pseudo-header. Maybe just put it in the Header map?

Unless we put some level of websocket bootstrap support directly into net/http, a websocket client is going to need to know whether it's going to be speaking HTTP/1 or HTTP/2, since the initial protocol bootstrap differs between the two. This is probably okay.

ethanpailes

ethanpailes commented on May 27, 2022

@ethanpailes

Yeah, I think those are all good points. The suggestion you made in the CL works with no changes to http2 for a GET request, but runs into SETTINGS_ENABLE_CONNECT_PROTOCOL trouble when I try to use CONNECT with it.

I've started poking at SETTINGS_ENABLE_CONNECT_PROTOCOL support a bit. It seems like it should be easy enough to just advertise it in the initial settings frame on the server side and then set it as a flag on the ClientConn on the server side. We probably don't even need to expose it to users on the client side, we could just check the flag and return an error if they try to send a CONNECT message and the setting is not enabled.

Currently, the client seems to clobber the scheme and path when you try sending a CONNECT request, which the server doesn't much like, so I'm trying to figure that out. I'm a little worried about the compatibility of no longer clobbering those parts of the URI though, since users might have come to rely on that behavior. Do you know what the reason for clobbering it like this is?

neild

neild commented on May 27, 2022

@neild
Contributor

HTTP/2 CONNECT explicitly doesn't allow the :scheme and :path pseudo-headers:
https://datatracker.ietf.org/doc/html/rfc7540#section-8.3

The SETTINGS_ENABLE_CONNECT_PROTOCOL parameter changes the semantics of CONNECT.

ClientConn.encodeHeaders should handle CONNECT requests with a :protocol pseudo-header differently from ones without--in particular, it should return an error if the server didn't set SETTINGS_ENABLE_CONNECT_PROTOCOL, and it should include the :path and :scheme if the server did.

64 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @neild@mitar@hexfusion@let4be@ethanpailes

        Issue actions

          proposal: x/net/http2: support for WebSockets over HTTP/2 · Issue #49918 · golang/go