Search code examples
httphttp2

How to ask the client to downgrade a HTTP2 connection to HTTP1.1


This is mostly out of curiosity; say I have a HTTP1.1 compatible server. I don't have the resources / it's not possible to add full HTTP2 support to that server.

Still I want to be able to serve clients which open a connection with a HTTP2 connection preface (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n) by telling them to use HTTP1.1 instead. Note that this is about clients which don't start the connection with a HTTP1.1 request with an Upgrade header.

This is the behavior I'd like to achieve:

CLIENT                 SERVER
  | ------ http2 GET --> |
  | <----need http1 ---- |
  | ------ http1 GET --> |
  ... (normal http1 processing)

The specification contains an error code HTTP_1_1_REQUIRED (0xd) which sounds just about what I need. Moreover, from reading around it seems that this error code is used to downgrade connections when necessary (TLS re-negotiation apparently?). Yet I just don't seem to be able to get this to work...

char const Http2EmptySettingsFrame[] =
  {
   // Frame Header
   0, 0, 0,
   0x4,
   0,
   0 & 0x7F, 0, 0, 0
  };

char const Http2RstStreamFrame[] =
  {
   // Frame Header
   0, 0, 4, // Length of frame content
   0x3, // Type: RST_STREAM
   0, // Flags
   0 & 0x7F, 0, 0, 1, // Stream identifier 1 ; also tried with 0

   // Frame content
   0, 0, 0, 0xD // Error code (RST_STREAM frame)
  };

char const Http2GoAwayFrame[] =
  {
   // Frame Header
   0, 0, 8,
   0x7,
   0,
   0 & 0x7F, 0, 0, 0,

   0 & 0x7F, 0, 0, 0, // GO_AWAY stream id
   0, 0, 0, 0xD // GO_AWAY error code: HTTP_1_1_REQUIRED
  };

Using the above and fwrite(frame, sizeof(frame), 1, stdout) in a small test program, I create frames to send to a client to (./a.out > frame.bin). I then start my "server":

cat emptysettings.bin rststream.bin goaway.bin - | netcat -l -p 8888 -s 127.0.0.1

Using a http2 client I then get ...

# this was when just sending empty SETTINGS and RST_STREAM frame
nghttp -v http://127.0.0.1:8888
[  0.000] Connected
[  0.000] recv SETTINGS frame <length=0, flags=0x00, stream_id=0>
          (niv=0)
[  0.000] [INVALID; error=Protocol error] recv RST_STREAM frame <length=4, flags=0x00, stream_id=1>
          (error_code=HTTP_1_1_REQUIRED(0x0d))
[  0.000] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.000] send GOAWAY frame <length=34, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=PROTOCOL_ERROR(0x01), opaque_data(26)=[RST_STREAM: stream in idle])
[ERROR] request http://127.0.0.1:8888 failed: request HEADERS is not allowed
Some requests were not processed. total=1, processed=0

... or using curl ...

curl -v --http2-prior-knowledge http://127.0.0.1:8888
*   Trying 127.0.0.1:8888...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8888 (#0)
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x16887f0)
> GET / HTTP/2
> Host: 127.0.0.1:8888
> User-Agent: curl/7.66.0
> Accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 4294967295)!
* stopped the pause stream!
* Connection #0 to host 127.0.0.1 left intact
curl: (16) Error in the HTTP2 framing layer

... thus: What response do I need to send to a client initiating a http2 connection for them to downgrade the connection to HTTP1.1?

Sending HTTP/1.1 505 hmmm\n\n (505 = HTTP Version not supported) doesn't help either...


Solution

  • What response do I need to send to a client initiating a http2 connection for them to downgrade the connection to HTTP1.1?

    I don't think what you are trying to do makes sense. Why are you accepting the HTTP/2 connection in the first place? Just reject it upfront. Why implement HTTP/2 just to then say you don't support HTTP/2?

    However if you really want to do this, then you need to close the connection with a GOAWAY frame and HTTP_1_1_REQUIRED error code. You cannot downgrade the connection.

    To give some more background to that answer, a lot of thought has gone into ensuring you do not establish an HTTP/2 connection if HTTP/2 is not supported. There are basically 3 methods of establishing an HTTP/2 connection:

    1. HTTPS - as part of the TLS negotiation using the ALPN extension. Which should only succeed if the server actually advertises HTTP/2 support.
    2. HTTP - as part of the upgrade dance - which should only succeed if the server actually advertises HTTP/2 support.
    3. HTTP - as part of a direct connection. Which seems to be what you are suggesting. This assumes HTTP/2 support, so is a bit more risky so should only be done when there is a high probability of knowing the server supports HTTP/2 (e.g. you own both the client and the server) and the client should be prepared to handle the case where the preface connection fails incase that knowledge turns out to be wrong.

    HTTPS is the vast majority of HTTP/2 use case (and certainly from browser which does not support HTTP/2 when not using HTTP) but all three methods have explicit checks as part of the connection establishment. So it should be impossible to have an HTTP/2 connection which only supports HTTP/1.1 (and at this point I struggle to see how that last sentence I wrote makes sense!). But hey stranger things have happened...

    So, ignoring that, and going with you for now, using the error code HTTP_1_1_REQUIRED on a RST_STREAM frame was intended to reject a single stream, not the whole connection. So a request that requires client certificates (not supported under HTTP/2 - though a proposal exists to add it) of websockets (originally not supported under HTTP/2 but since added) should reject that requests with this error code, with a RST_STREAM and that error code. The connection should be left in place so any other HTTP/2 compatible request can be made. You could in theory just reject every stream request in the same way but seems a bit silly, and inefficient, to bother establishing a HTTP/2 connection solely for the purpose of saying "Please use HTTP/1.1" for every request it sends.

    If you want to reject the whole connection, then you should not use the RST_STREAM frame (which is not allowed on stream id 0) but instead use a GOAWAY frame (which must be sent on stream 0). This GOAWAY message can use the error code HTTP_1_1_REQUIRED. This will close down the whole connection and the client will need to reconnect - at which point, as per above you should not accept the HTTP/2 connection. It is not possible to downgrade a connection. Well technically it could use the upgrade methods to do that, but that is a suggestion, not a command.

    So, with that behind us let's look at your code and the errors:

       0x3, // Type: RST_STREAM
       0, // Flags
       0 & 0x7F, 0, 0, 1, // Stream identifier 1 ; also tried with 0
    

    Well for a start you can't use RST_STREAM on stream 0 so you should only be sending this on a stream id > 0:

    RST_STREAM frames MUST be associated with a stream. If a RST_STREAM frame is received with a stream identifier of 0x0, the recipient MUST treat this as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.

    But you could use this to reject each stream as it comes in if you want as per above.

    [INVALID; error=Protocol error] recv RST_STREAM frame <length=4, flags=0x00, stream_id=1>
          (error_code=HTTP_1_1_REQUIRED(0x0d))
    

    It seems to me you sent a RST_STREAM before the client even had a chance to establish that stream! The client hadn't even sent its first SETTINGS frame (required by HTTP/2 protocol as the first message). You can't reset a stream before it's established. Wait until the GET request comes in, and then reject it with a RST_STREAM and it should try again on a separate HTTP/1.1 connection. This will NOT downgrade the connection but just reject that one stream as per above.