kaspterio code as a hacker

01 October 2019

1 Starting HTTP/2

  • h2
  • h2c

Starting HTTP/2 for “http” URIs

client需要通过http1.1的upgrade header告诉server (值为h2c),进行协议升级,同时通过http2-settings header把HTTP/2 SETTINGS的payload base64url后的值告诉server,这样的设计让client 可以在进行http2通信前告诉server一些参数 ???

server端如果支持http2, 则需要接受upgrade,返回101状态(Swtiching Protocols),在空行后开始传输http2二进制帧数据。

一旦client收到server的101 response,就需要发送connection preface(包括SETTINGS frame)

如果client知道server一定支持http2,那么就可以直接发送connection preface,省去了upgrade这个rtt

Starting HTTP/2 for “https” URIs

用TLS的ALPN extension来确定http2协议,一旦TLS协商完成后,client和server都需要发送connection preface

Connection preface

是为了让两端都确定通信的协议以及一些通信的初始参数设定

conenction preface由一段特殊的固定字符串开始,紧跟着是SETTINGS frame。

client端要么在101 response后开始发送preface,要么在TLS的第一个appliaction data中开始发送。如果知道server端是支持http2的,那么直接就可以发送connection preface

client端发送完connection preface后直接就能开始发送后续的数据frame,不需要等待server端的conection preface。当client收到server的conneciton preface后需要去尊重这些参数,可能需要去变更一些配置 (这点和tcp有些不一样)

2 HTTP frames

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+

header占了9个字节。分别是

  • payload的length,无符号24位整数,最大值为16384;
  • 8-bit的type,frame的类型,决定怎么去解析frame payload
  • 8-bit的flags,目前保留
  • 一个bit的R,保留
  • 31-bit的Stream identifier,唯一标识一个connection中的stream

Header Compression and Decompression

HEADERS、PUSH_PROMISE、CONTINUATION三种frame中都会包括http header的内容,其中HEADERS是client向server发送数据时的header内容,PUSH_PROMISE是server主动push给client数据时的http header内容,CONTINUATION则必须要跟着HEADERS或者PUSH_PROMISE一起出现,用于当一帧发不完时候继续帧。

Header list是header的key-value集合,http2中将header list压缩成header block,header block会被划分成一个或者多个header block fragments,作为HEADERS、PUSH_PROMIST、CONTINUATION的payload 接收端需要组装收到的这些fragments,然后decompress来重建header list.

Header compression 是有状态的,一个compression context和一个decompression context作用于整个connection生命周期。

Header blocks 必须要作为连续的frames来传输,中间不能有其他类型的frame或者其他的stream的frame穿插进来。

3 Stream and multiplexing

3.1 Stream状态机


                                +--------+
                        send PP |        | recv PP
                       ,--------|  idle  |--------.
                      /         |        |         \
                     v          +--------+          v
              +----------+          |           +----------+
              |          |          | send H /  |          |
       ,------| reserved |          | recv H    | reserved |------.
       |      | (local)  |          |           | (remote) |      |
       |      +----------+          v           +----------+      |
       |          |             +--------+             |          |
       |          |     recv ES |        | send ES     |          |
       |   send H |     ,-------|  open  |-------.     | recv H   |
       |          |    /        |        |        \    |          |
       |          v   v         +--------+         v   v          |
       |      +----------+          |           +----------+      |
       |      |   half   |          |           |   half   |      |
       |      |  closed  |          | send R /  |  closed  |      |
       |      | (remote) |          | recv R    | (local)  |      |
       |      +----------+          |           +----------+      |
       |           |                |                 |           |
       |           | send ES /      |       recv ES / |           |
       |           | send R /       v        send R / |           |
       |           | recv R     +--------+   recv R   |           |
       | send R /  `----------->|        |<-----------'  send R / |
       | recv R                 | closed |               recv R   |
       `----------------------->|        |<----------------------'
                                +--------+

          send:   endpoint sends this frame
          recv:   endpoint receives this frame

          H:  HEADERS frame (with implied CONTINUATIONs)
          PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
          ES: END_STREAM flag
          R:  RST_STREAM frame

HTTP request/response 机制

再开始介绍stream状态机之前需要先了解下http2的message exchange机制,http2设计上为了最大程度兼容目前http的语义,因为目前的request/response语义被保留了下来。通过在一个http2 stream上按照一定的约束交换各种http2 frame,达到组成语义上实现request/response的目的,这就构成http2的message,一个message通过由如下几个部分构成:

  1. (response only) 0到多个HEADERS frames(每个都可以跟随0到多个CONTINUATION frames)组成informational (1xx) http responses中的message header部分
  2. 一个HEADERS frame (可能跟随0到多个CONTINUATION frames) 组成message headers
  3. 0到多个DATA frames 组成request/response的消息体
  4. 可选的一个HEADERS frame(可能跟随0到多个CONTINUATION frames)组成trailer-part

一般来说就只有2和3

一个完整的http2 stream则包括一个完整的http request/response交换,因此正常的stream的状态一般来说有idle -> open -> half close -> close。

一个request message从HEADERS frame开始(将stream从idle状态切到open状态),到一个带END_STREAM的frame结束(将stream切到half-closed状态, 对于client来说是half-closed local, 对于server来说是half-closed remote)。

一个response message从HEADERS frame开始到一个带END_STREAN的frame结束(同时会将stream置为closed状态)。

HTTP Server Push机制

http2允许server根据client之前发起的request来先发制人的发送一些responses (和相应的“承诺过”的requests一起)给client。Server push在语义上等价于server去响应一个request,只不过这个request也是server发送的(通过PUSH_PROMISE帧发送这个request)。

PUSH_PROMISE frame和HEADERS frame一样,payload是http header block,而且要包括所有的必要的header fields。区别在于,HEADERS是会打开一个新的stream,而PUSH_PROMISE则是在之前的client发起的stream上发送。在payload里面包括一个promised stream identifer作为一个后面新的Server push stream。另外,还有一点对于理解什么是PUSH_PROMISE frame很重要的是PUSH_PROMISE是分散在组成http response的各个帧之间的,比如:client请求一个包括多个图片link的富文档,server在响应这个request的response中可能会穿插多个PUSH_PROMISE帧,每个图片link一个PUSH_PROMISE,这些PUSH_PROMIISE帧发送在富文档stream上,意图在于告诉client端,我后面会主动地push给你这些图片内容。这也是叫promise的原因。

server发送完PUSH_PROMISE之后,就可以开始交付push response了(相对自己说的话,现在来兑现了),这个response将会在新的stream上发送(就是之前PUSH_PROMISE frame里指定的promised stream identifer上发送),另外push response message的构成和普通的http response message的构成完全一样。区别在于对stream state的变更时机不太一样,普通的http response message在发送之前stream就已经处于half closed 状态了,但是push response不太一样,在push response开始之前,stream处于reserved状态,所以当push response发送完HEADERS帧后需要做一次stream state切换,从reserved状态切换到half-closed状态。

3.2 Stream Identifiers

stream id有如下几个属性

  • 31-bit integer
  • client发起的stream id需要是奇数,server发起的stream (Server push) id需要是偶数
  • 0x0用于connection control帧的id,不能用来作为一个新的stream的id
  • 新的stream的id必须要大于之前所有opened或者reserved的stream的id
  • 同一个connection上的stream id不能够被复用,对于那些long-lived connections如果耗尽stream id之后,client可以去重新建立一个connection,server可以去发送一个GOAWAY帧告诉client强制新建一个connection。

关于stream的并发度再介绍一点: SETTINGS frame中有个参数可以限制允许一端最大同时发起的stream个数(SETTINGS_MAX_CONCURRENT_STREAMS)。要注意的是这个值是针对各自端的限制,如果client端想要关闭server push功能,那么可以在发给server的SETTIINGS帧中把SETTINGS_MAX_CONCURRENT_STREAMS置为0,意思就是不允许server端发起stream。只有处于open或者half-closed状态的stream才算做是并发的stream,reserved状态的stream还不算(因为还没有开始去发送push response)。

3.3 Flow control

http2的flow control主要有以下几个特点:

  • flow control作用于hop-to-hop,不是作用于整个end-to-end
  • credit-based scheme:接收方通过WINDOW_UPDATE帧告诉发送方目前在当前stream或者connection上还能接受多少字节来驱动。client、server、所有的中间件作为接收方时需要通过这样的方式来告诉对端自己的flow-control window大小,作为发送时需要去遵守flow-control的限制
  • 初始值是65535,对所有stream和整个connection都是
  • 只有DATA帧才被flow control,其他的帧不受限制
  • 协议中没有规定什么时候应该就发送WINDOW_UPDATE帧,具体算法取决于HTTP2实现。
  • flow-control不能被禁用,但是如果有些场景不需要这个能力,那么可以让实现设置一个最大的窗口(2^31 -1),同时收到任何数据后都回复一个WINDOW_UPDATE帧来维持最大窗口
  • flow control作用于connection维度以及stream维度。

3.4 Stream priority

why

priority的引入有两个目的:

  • 为了让endpoint能够表达他希望对端来怎么对并发的stream进行分配资源,
  • 更重要的是当endpoint的数据发送能力受限时,priority能够用来决定优先选择发送哪些streams的frame

what

那么怎么来表达stream的priority呢,HTTP2通过依赖树来表达stream的优先级,如果stream B依赖于Stream A,那么A的优先级就高于B。那问题来了,如果有多个streams B、C、D都依赖于A,HTTP2通过引入dependency weight来描述依赖于同一个stream的多个流之间的权重,这个权重决定了这些个流分配资源的比例。这样将当前所有的并发的流形成了一颗依赖树,root是stream 0,每个stream只有一个父依赖,每个stream可以被多个子stream依赖。

问题: 怎么表达一个stream依赖多个stream的情况???

how

怎么设置stream dependency呢?

在创建流的时候指定stream的依赖

在HEADERS frame中指定dependency和weight

HEADERS frame format:

    +---------------+
    |Pad Length? (8)|
    +-+-------------+-----------------------------------------------+
    |E|                 Stream Dependency? (31)                     |
    +-+-------------+-----------------------------------------------+
    |  Weight? (8)  |
    +-+-------------+-----------------------------------------------+
    |                   Header Block Fragment (*)                 ...
    +---------------------------------------------------------------+
    |                           Padding (*)                       ...
    +---------------------------------------------------------------+

其中E flag标识该stream是排他性依赖父stream的,如果父stream目前有子stream依赖,那么所有的子依赖将成为当前stream的依赖。

创建流后的任何时候都可以去更改stream的依赖

在PRIORITY frame中指定dependency和weight

PRIORITY frame format:

    +-+-------------------------------------------------------------+
    |E|                  Stream Dependency (31)                     |
    +-+-------------+-----------------------------------------------+
    |   Weight (8)  |
    +-+-------------+

值得注意的是PRIORITY帧可以在任何时候发送,也就是无论当前stream处于什么状态,都可以通过PRIORITY来更改stream的依赖状态。如果当前stream处于idle或者closed状态,主要作用是为了能够对依赖他的所有子stream进行Reprioritization。也就是说既可以对一个idle/closed的stream进行依赖变更,也可以让一个stream去depends-on一个idle/closed的stream。

Stream Reprioritization

这里协议规定了当stream的dependency发生变化时一些case的处理

  • case: 父stream的dependency发生变化,所有的子依赖树都跟着移动就行,一般不需要做子树结构的调整
  • case: 如果变更某个stream的依赖时,加上了排他性依赖,那么这个stream的新的父stream的所有子依赖stream都将成为该stream的子依赖stream。
  • case: 如果变更一个stream的依赖去依赖自己的某个子stream,这个情况比较特殊,需要先把这个子stream改为依赖当前stream的之前的父stream,然后再变更当前stream的依赖。

例子:(让A重新依赖子stream D)

       ?                ?                ?                 ?
       |               / \               |                 |
       A              D   A              D                 D
      / \            /   / \            / \                |
     B   C     ==>  F   B   C   ==>    F   A       OR      A
        / \                 |             / \             /|\
       D   E                E            B   C           B C F
       |                                     |             |
       F                                     E             E
                  (intermediate)   (non-exclusive)    (exclusive)

Prioritization state management

为了能够正确处理流的dependency信息的改变(术语就是Reprioritization),每端都需要维护stream的依赖状态(prioritization state)也就是维护依赖树。比如当一个stream从依赖树中删除时,那么可以把他的所有的子stream移动成为该stream的父stream的子,这时候这些被移动的子stream的weight需要根据被删除的stream原来的weight进行重新计算。那么什么时候会去从依赖树里删除一个stream呢?这要取决于具体的实现,因为存在有些stream会去依赖一个已经closed的流,所以依赖树的状态要能够尽量保留这些已经closed的stream。

Default Priorities

所有的stream都默认depends on Stream 0, Pushed Streams则depends on他们关联的client端发起的stream,另外所有的stream的weight默认值都是16

4 Frame definitions

DATA

    +---------------+
    |Pad Length? (8)|
    +---------------+-----------------------------------------------+
    |                            Data (*)                         ...
    +---------------------------------------------------------------+
    |                           Padding (*)                       ...
    +---------------------------------------------------------------+
  • padding是为了混淆消息的size,是HTTP2的安全特性,只要当flag里PADDED位标志设置时才启用
  • DATA帧受flow control的控制,只能在stream处于open/half-closed remote状态下才能发送

HEADERS

    +---------------+
    |Pad Length? (8)|
    +-+-------------+-----------------------------------------------+
    |E|                 Stream Dependency? (31)                     |
    +-+-------------+-----------------------------------------------+
    |  Weight? (8)  |
    +-+-------------+-----------------------------------------------+
    |                   Header Block Fragment (*)                 ...
    +---------------------------------------------------------------+
    |                           Padding (*)                       ...
    +---------------------------------------------------------------+

  • HEADERS帧用来open一个stream,同时带上header block fragment,只能在stream处于idle/reserved local/open/half-closed remote状态时才能发送
  • E/Stream dependency/weight在上一节已经介绍过了,用来表示stream的priority
  • Header block 如果不能完全在一个HEADERS里装下的话,会通过CONTNUATION帧来继续发送header block fragment,最后一帧的END_HEADERS flag需要被置位
  • HEADERS也需要有padding来保护

PRIORITY

    +-+-------------------------------------------------------------+
    |E|                  Stream Dependency (31)                     |
    +-+-------------+-----------------------------------------------+
    |   Weight (8)  |
    +-+-------------+

  • 用于发送端来建议stream的priority。
  • 可以在stream处于什么状态时发送。当stream处于idle/closed状态时,发送PRIORITY帧允许对所有依赖该stream的子stream进行reprioritization。

RST_STREAM

    +---------------------------------------------------------------+
    |                        Error Code (32)                        |
    +---------------------------------------------------------------+
  • 用于请求取消一个stream,或者标识一个错误的发生。error code标识错误的原因

SETTINGS

  • 用来传输影响通信的配置参数,比如偏好、对端的约束,同时也用来去acknowledge收到的来自对端的SETTINGS,什么时候去ack对端的Settings呢?协议规定收到SETTINGS帧后,一旦所有的配置值都处理完后,接收方必须立即回复SETTINGS ack,发送方收到ack后,就可以应用新的配置值了,如果发送方在一定时间内没有收到ack,那么可能会抛出一个连接错误(SETTINGS_TIMEOUT)
  • 连接一开始双方就必须要发送SETTINGS来描述各自的通信配置,在连接的整个生命周期内也都可以再发送SETTINGS帧来更改覆盖配置。
  • 要注意SETTINGS是作用于整个连接,不是针对某个stream,所以frame header的stream id必须要是0

SETTINGS payload

payload包含0到多个参数配置,每个参数配置的格式如下:

    +-------------------------------+
    |       Identifier (16)         |
    +-------------------------------+-------------------------------+
    |                        Value (32)                             |
    +---------------------------------------------------------------+

具体参数配置这里就不写了,参考协议文档

PUSH_PROMISE

    +---------------+
    |Pad Length? (8)|
    +-+-------------+-----------------------------------------------+
    |R|                  Promised Stream ID (31)                    |
    +-+-----------------------------+-------------------------------+
    |                   Header Block Fragment (*)                 ...
    +---------------------------------------------------------------+
    |                           Padding (*)                       ...
    +---------------------------------------------------------------+
  • PUSH_PROMISE的作用前面的内容也提到过,就是为了提前通知对端后面会发送一个stream,stream的一些信息会在PUSH_PROMISE的header block 中给出。
  • 必须要发送在那些处于open/half-closed remote状态上的stream
  • SETTINGS里有两个设置可以去disable掉server push,一个是之前提到的SETTINGS_MAX_CONCURRENT_STREAMS,还有就是更直接的SETTINGS_ENABLE_PUSH

PING

    +---------------------------------------------------------------+
    |                                                               |
    |                      Opaque Data (64)                         |
    |                                                               |
    +---------------------------------------------------------------+
  • 发送端为了测量RTT。同时检测连接是否还正常
  • 必须只能携带8个字节的自定义数据
  • PING的接收方必须要要回复一个ack PING帧(PING帧的ack flag置位)
  • PING帧和SETTINGS帧一样,作用于整个connection,不属于某一个stream

GOAWAY

    +-+-------------------------------------------------------------+
    |R|                  Last-Stream-ID (31)                        |
    +-+-------------------------------------------------------------+
    |                      Error Code (32)                          |
    +---------------------------------------------------------------+
    |                  Additional Debug Data (*)                    |
    +---------------------------------------------------------------+
  • 用于告知对端去shutdown一个连接,做到gracefully stop。
  • 发送GOAWAY帧和对端开启一个新的stream之间可能存在race condition,因此GOAWAY帧里包含了一个stream id用来标识最后一个被处理了的对端发起的stream id。一旦发了GOAWAY,发送方在接受到比last-stream-id大的stream id时就可以安全的忽略掉,同样,接收方在接受到GOAWAY帧后,就可以知道那些比last-stream-id大的stream可能没有被处理,需要后面在新的连接里重试
  • 同SETTINGS/PING一样,GOWAY也是作用于整个connection,不针对某个stream

WINDOW_UPDATE

    +-+-------------------------------------------------------------+
    |R|              Window Size Increment (31)                     |
    +-+-------------------------------------------------------------+
  • WINDOW_UPDATE就是为了实现flow control的,关于流控前面已经介绍过了,协议里只有一个保留位和一个31bit的无符号整数标识对端能够在当前窗口值基础上新增传输多少字节数据。
  • 可以作用于某个stream上,也可以作用于整个connection上

flow control window

  • 实现上要为每个stream维护一个当前窗口值,标识发送方目前能够发送多少数据,整个值也是对接收方buffering容量的度量。同时需要为整个conection维护一个窗口值,发送方实际能发送数据量的多少不能超过这两个窗口值。
  • 一旦发送了一个flow-controlled frame,发送方就需要从两个窗口值中减去发送的frame size
  • 接收方收到数据并且消费数据后,就可以去通过WINDOW_UPDATE帧告诉发送方增加对应窗口,stream-level和connection-level的窗口需要各自的WINDOW_UPDATE帧
  • 发送方收到来自接收方的WINDOW_UPDATE后需要更新对应的窗口值

初始窗口值

  • 默认的的窗口初始值都是65535
  • SETTINGS帧里提供了一个配置参数可以去调整窗口初始值——SETTINGS_INITIAL_WINDOW_SIZE
  • 由于SETTINGS还可以在stream处于open/half-closed remote状态下发送,也就是流上可能已经按照之前的窗口初始值传输过数据了,意味着如果SETTINGS帧里可以修改这个SETTINGS_INITIAL_WINDOW_SIZE, SETTINGS的接收方需要根据新的初始值和流建立时候的初始值来调整当前窗口,这可能会导致当前窗口变为负值,此时数据发送方就不能再发送数据了,只到收到WINDOW_UPDATE帧使得窗口回到正数。

CONTINUATION

    +---------------------------------------------------------------+
    |                   Header Block Fragment (*)                 ...
    +---------------------------------------------------------------+
  • 用在继续传输header block fragments,作为HEADERS、PUSH_PROMISE帧的后续