summaryrefslogtreecommitdiffstats
path: root/doc/internals/api/htx-api.txt
blob: 971328bb92e4ed8cb24e5f06e35a56c432fff702 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
                -----------------------------------------------
                                   HTX API
                                  Version 1.1
                          ( Last update: 2021-02-24 )
                -----------------------------------------------
                          Author : Christopher Faulet
                      Contact : cfaulet at haproxy dot com

1. Background

Historically, HAProxy stored HTTP messages in a raw fashion in buffers, keeping
parsing information separately in a "struct http_msg" owned by the stream. It was
optimized to the data transfer, but not so much for rewrites. It was also HTTP/1
centered. While it was the only HTTP version supported, it was not a
problem. But with the rise of HTTP/2, it starts to be hard to still use this
representation.

At the first age of the HTTP/2 in HAProxy, H2 messages were converted into
H1. This was terribly unefficient because it required two parsing passes, a
first one in H2 and a second one in H1, with a conversion in the middle. And of
course, the same was also true in the opposite direction. outgoing H1 messages
had to be converted back in H2 to be sent. Even worse, because the H2->H1
conversion, only client H2 connections were supported.

So, to address all these problems, we decided to replace the old raw
representation by a version-agnostic and self-structured internal HTTP
representation, the HTX. As an additional benefit, with this new representation,
the message parsing and its processing are now separated, making all the HTTP
analysis simpler and cleaner. The parsing of HTTP messages is now handled by
the multiplexers (h1 or h2).


2. The HTX message

The HTX is a structure containing useful information about an HTTP message
followed by a contiguous array with some parts of the message. These parts are
called blocks. A block is composed of metadata (htx_blk) and an associated
payload. Blocks' metadata are stored starting from the end of the array while
their payload are stored at the beginning. Blocks' metadata are often simply
called blocks. it is a misuse of language that's simplify explanations.

Internally, this structure is "hidden" in a buffer. This way, there are few
changes into intermediate layers (stream-interface and channels). They still
manipulate buffers. Only the multiplexer and the stream have to know how data
are really stored. From the HTX perspective, a buffer is just a memory
area. When an HTX message is stored in a buffer, this one appears as full.

  * General view of an HTX message :


  buffer->area
    |
    |<------------ buffer->size == buffer->data ----------------------|
    |                                                                 |
    |     |<------------- Blocks array (htx->size) ------------------>|
    V     |                                                           |
    +-----+-----------------+-------------------------+---------------+
    | HTX |   PAYLOADS ==>  |                         |  <== HTX_BLKs |
    +-----+-----------------+-------------------------+---------------+
          |                 |                         |               |
          |<-payloads part->|<----- free space ------>|<-blocks part->|
              (htx->data)


The blocks part remains linear and sorted. It may be see as an array with
negative indexes. But, instead of using negative indexes, we use positive
positions to identify a block. This position is then converted to an address
relatively to the beginning of the blocks array.

                tail                                 head
                 |                                     |
                 V                                     V
     .....--+----+-----------------------+------+------+
            | Bn |       ...             |  B1  |  B0  |
     .....--+----+-----------------------+------+------+
                 ^                       ^      ^
 Addr of the block       Addr of the block      Addr of the block
 at the position N       at the position 1      at the position 0


In the HTX structure, 3 "special" positions are stored :

    - tail  : Position of the newest inserted block
    - head  : Position of the oldest inserted block
    - first : Position of the first block to (re)start the analyse

The blocks part never wrap. If we have no space to allocate a new block and if
there is a hole at the beginning of the blocks part (so at the end of the blocks
array), we move back all blocks.


      tail           head                                tail           head
       |              |                                   |              |
       V              V                                   V              V
    ...+--------------+---------+    blocks  ...----------+--------------+
       | X== HTX_BLKS |         |    defrag               | <== HTX_BLKS |
    ...+--------------+---------+    =====>  ...----------+--------------+


The payloads part is a raw space that may wrap. A block's payload must never be
accessed directly. Instead a block must be selected to retrieve the address of
its payload.


          +------------------------( B0.addr )--------------------------+
          |    +-------------------( B1.addr )----------------------+   |
          |    |       +-----------( B2.addr )----------------+     |   |
          V    V       V                                      |     |   |
    +-----+----+-------+----+--------+-------------+-------+----+----+----+
    | HTX | P0 |   P1  | P2 | ...==> |             | <=... | B2 | B1 | B0 |
    +-----+----+-------+----+--------+-------------+-------+----+----+----+


Because the payloads part may wrap, there are 2 usable free spaces :

    - The free space in front of the blocks part. This one is used if and only if
      the other one was not used yet.

    - The free space at the beginning of the message. Once this one is used, the
      other one is never used again, until a message defragmentation.


  * Linear payloads part :


      head_addr             end_addr     tail_addr
          |                    |             |
          V                    V             V
    +-----+--------------------+-------------+--------------------+-------...
    | HTX |                    |   PAYLOADS  |                    | HTX_BLKs
    +-----+--------------------+-------------+--------------------+-------...
          |<-- free space 2 -->|             |<-- free space 1 -->|
     (used if the other is too small)          (used in priority)


  * Wrapping payloads part :


                            head_addr end_addr        tail_addr
                                |        |                |
                                V        V                V
    +-----+----+----------------+--------+----------------+-------+-------...
    | HTX |    | PAYLOADS part2 |        | PAYLOADS part1 |       | HTX_BLKs
    +-----+----+----------------+--------+----------------+-------+-------...
          |<-->|                |<------>|                |<----->|
         unusable               free space                 unusable
        free space                                        free space


Finally, when the usable free space is not enough to store a new block, unusable
parts may be get back with a full defragmentation. The payloads part is then
realigned at the beginning of the blocks array and the free space becomes
continuous again.


3. The HTX blocks

An HTX block can be as well a start-line as a header, a body part or a
trailer. For all these types of block, a payload is attached to the block. It
can also be a marker, the end-of-headers or end-of-trailers. For these blocks,
there is no payload but it counts for a byte. It is important to not skip it
when data are forwarded.

As already said, a block is composed of metadata and a payload. Metadata are
stored in the blocks part and are composed of 2 fields :

    - info : It a 32 bits field containing the block's type on 4 bits followed
             by the payload length. See below for details.

    - addr : The payload's address, if any, relatively to the beginning the
             array used to store part of the HTTP message itself.


  * Block's info representation :

    0b 0000 0000 0000 0000 0000 0000 0000 0000
       ---- ------------------------ ---------
       type     value (1 MB max)     name length (header/trailer - 256B max)
            ----------------------------------
                 data length (256 MB max)
     (body, method, path, version, status, reason)


Supported types are :

    - 0000  (0) : The request start-line
    - 0001  (1) : The response start-line
    - 0010  (2) : A header block
    - 0011  (3) : The end-of-headers marker
    - 0100  (4) : A data block
    - 0101  (5) : A trailer block
    - 0110  (6) : The end-of-trailers marker
    - 1111 (15) : An unused block

Other types are unused for now and reserved for futur extensions.

An HTX message is typically composed of following blocks, in this order :

    - a start-line
    - zero or more header blocks
    - an end-of-headers marker
    - zero or more data blocks
    - zero or more trailer blocks (optional)
    - an end-of-trailers marker (optional but always set if there is at least
      one trailer block)

Only one HTTP request at a time can be stored in an HTX message. For HTTP
response, it is more complicated. Only one "final" response can be stored in an
HTX message. It is a response with status-code 101 or greater or equal to
200. But it may be preceded by several 1xx informational responses. Such
responses are part of the same HTX message.

When the end of the message is reached a special flag is set on the message
(HTX_FL_EOM). It means no more data are expected for this message, except
tunneled data. But tunneled data will never be mixed with message data to avoid
ambiguities. Thus once the flag marking the end of the message is set, it is
easy to know the message ends. The end is reached if the HTX message is empty or
on the tail HTX block in the HTX message. Once all blocks of the HTX message are
consumed, tunneled data, if any, may be transferred.


3.1. The start-line

Every HTX message starts with a start-line. Its payload is a "struct htx_sl". In
addition to the parts of the HTTP start-line, this structure contains some
information about the represented HTTP message, mainly in the form of flags
(HTX_SL_F_*). For instance, if an HTTP message contains the header
"conten-length", then the flag HTX_SL_F_CLEN is set.

Each HTTP message has its own start-line. So an HTX request has one and only one
start-line because it must contain only one HTTP request at a time. But an HTX
response may have more than one start-line if the final HTTP response is
precedeed by some 1xx informational responses.

In HTTP/2, there is no start-line. So the H2 multiplexer must create one when it
converts an H2 message to HTX :

    - For the request, it uses the pseudo headers ":method", ":path" or
      ":authority" depending on the method and the hardcoded version "HTTP/2.0".

    - For the response, it used the hardcoded version "HTTP/2.0", the
      pseudo-header ":status" and an empty reason.


3.2. The headers and trailers

HTX Headers and trailers are quite similar. Different types are used to simplify
headers processing. But from the HTX point of view, there is no real difference,
except their position in the HTX message. The header blocks always follow an HTX
start-line while trailer blocks come after the data. If there is no data, they
follow the end-of-headers marker.

Headers and trailers are the only blocks containing a Key/Value payload. The
corresponding end-of marker must always be placed after each group to mark, as
it name suggests, the end.

In HTTP/1, trailers are only present on chunked messages. But chunked messages
do not always have trailers. In this case, the end-of-trailers block may or may
not be present. Multiplexers must be able to handle both situations. In HTTP/2,
trailers are only present if a HEADERS frame is sent after DATA frames.


3.3. The data

The payload body of an HTTP message is stored as DATA blocks in the HTX
message. For HTTP/1 messages, it is the message body without the chunks
formatting, if any. For HTTP/2, it is the payload of DATA frames.

The DATA blocks are the only HTX blocks that may be partially processed (copied
or removed). All other types of block must be entierly processed. This means
DATA blocks can be resized.


3.4. The end-of markers

These blocks are used to delimit parts of an HTX message. It exists two
markers :

    - end-of-headers (EOH)
    - end-of-trailers (EOT)

EOH is always present in an HTX message. EOT is optional.


4. The HTX API


4.1. Get/set HTX message from/to the underlying buffer

The first thing to do to process an HTX message is to get it from the underlying
buffer. There are 2 functions to do so, the second one relying on the first :

    - htxbuf() returns an HTX message from a buffer. It does not modify the
      buffer. It only initialize the HTX message if the buffer is empty.

    - htx_from_buf() uses htxbuf(). But it also updates the underlying buffer so
      that it appears as full.

Both functions return a "zero-sized" HTX message if the buffer is null. This
way, the HTX message is always valid. The first function is the default function
to use. The second one is only useful when some content will be added. For
instance, it used by the HTX analyzers when HAProxy generates a response. Thus,
the buffer is in a right state.

Once the processing done, if the HTX message has been modified, the underlying
buffer must be also updated, except htx_from_buf() was used _AND_ data was only
added. For all other cases, the function htx_to_buf() must be called.

Finally, the function htx_reset() may be called at any time to reset an HTX
message. And the function buf_room_for_htx_data() may be called to know if a raw
buffer is full from the HTX perspective. It is used during conversion from/to
the HTX.


4.2. Helpers to deal with free space in an HTX message

Once with an HTX message, following functions may help to process it :

    - htx_used_space() and htx_meta_space() return, respectively, the total
      space used in an HTX message and the space used by block's metadata only.

    - htx_free_space() and htx_free_data_space() return, respectively, the total
      free space in an HTX message and the free space available for the payload
      if a new HTX block is stored (so it is the total free space minus the size
      of an HTX block).

    - htx_is_empty() and htx_is_not_empty() are boolean functions to know if an
      HTX message is empty or not.

    - htx_get_max_blksz() returns the maximum size available for the payload,
      not exceeding a maximum, metadata included.

    - htx_almost_full() should be used to know if an HTX message uses at least
      3/4 of its capacity.


4.3. HTX Blocks manipulations

Once the available sapce in an HTX message is known, the next step is to add HTX
blocks. First of all the function htx_nbblks() returns the number of blocks
allocated in an HTX message. Then, there is an add function per block's type :

    - htx_add_stline() adds a start-line. The type (request or response) and the
      flags of the start-line must be provided, as well as its three parts
      (method,uri,version or version,status-code,reason).

    - htx_add_header() and htx_add_trailers() are similar. The name and the
      value must be provided. The inserted HTX block is returned on success or
      NULL if an error occurred.

    - htx_add_endof() must be used to add any end-of marker. The block's type
      (EOH or EOT) must be specified. The inserted HTX block is returned on
      success or NULL if an error occurred.

    - htx_add_all_headers() and htx_add_all_trailers() add, respectively, a list
      of headers and a list of trailers, followed by the appropriate end-of
      marker. On success, this marker is returned. Otherwise, NULL is
      returned. Note there is no rollback on the HTX message when an error
      occurred. Some headers or trailers may have been added. So it is the
      caller responsibility to take care of that.

    - htx_add_data() must be used to add a DATA block. Unlike previous
      functions, this one returns the number of bytes copied or 0 if nothing was
      copied. If possible, the data are appended to the tail block if it is a
      DATA block. Only a part of the payload may be copied because this function
      will try to limit the message defragmentation and the wrapping of blocks
      as far as possible.

    - htx_add_data_atonce() must be used if all data must be added or nothing.
      It tries to insert all the payload, this function returns the inserted
      block on success.  Otherwise it returns NULL.

When an HTX block is added, it is always the last one (the tail). But, if a
block must be added at a specific place, it is not really handy. 2 functions may
help (others could be added) :

    - htx_add_last_data() adds a DATA block just after all other DATA blocks and
      before any trailers and EOT marker. It relies on htx_add_data_atonce(), so
      a defragmentation may be performed.

    - htx_move_blk_before() moves a specific block just after another one. Both
      blocks must already be in the HTX message and the block to move must
      always be placed after the "pivot".

Once added, there are three functions to update the block's payload :

    - htx_replace_stline() updates a start-line. The HTX block must be passed as
      argument. Only string parts of the start-line are updated by this
      function. On success, it returns the new start-line. So it is pretty easy
      to update its flags. NULL is returned if an error occurred.

    - htx_replace_header() fully replaces a header (its name and its value) by a
      new one. The HTX block must be passed a argument, as well as its new name
      and its new value. The new header can be smaller or larger than the old
      one. This function returns the new HTX block on success, or NULL is an
      error occurred.

    - htx_replace_blk_value() replaces a part of a block's payload or its
      totality. It works for HEADERS, TRAILERS or DATA blocks. The HTX block
      must be provided with the part to remove and the new one. The new part can
      be smaller or larger than the old one. This function returns the new HTX
      block on success, or NULL is an error occurred.

    - htx_change_blk_value_len() changes the size of the value. It is the caller
      responsibility to change the value itself, make sure there is enough space
      and update allocated value. This function updates the HTX message
      accordingly.

    - htx_set_blk_value_len() changes the size of the value. It is the caller
      responsibility to change the value itself, make sure there is enough space
      and update allocated value. Unlike the function
      htx_change_blk_value_len(), this one does not update the HTX message. So
      it should be used with caution.

    - htx_cut_data_blk() removes <n> bytes from the beginning of a DATA
      block. The block's start address and its length are adjusted, and the
      htx's total data count is updated. This is used to mark that part of some
      data were transferred from a DATA block without removing this DATA
      block. No sanity check is performed, the caller is responsible for doing
      this exclusively on DATA blocks, and never removing more than the block's
      size.

    - htx_remove_blk() removes a block from an HTX message. It returns the
      following block or NULL if it is the tail block.

Finally, a block may be removed using the function htx_remove_blk(). This
function returns the block following the one removed or NULL if it is the tail
block.


4.4. The HTX start-line

Unlike other HTX blocks, the start-line is a bit special because its payload is
a structure followed by its three parts :

        +--------+-------+-------+-------+
        | HTX_SL | PART1 | PART2 | PART3 |
        +--------+-------+-------+-------+

Some macros and functions may help to manipulate these parts :

    - HTX_SL_P{N}_LEN() and HTX_SL_P{N}_PTR() are macros to get the length of a
      part and a pointer on it. {N} should be 1, 2 or 3.

    - HTX_SL_REQ_MLEN(), HTX_SL_REQ_ULEN(), HTX_SL_REQ_VLEN(),
      HTX_SL_REQ_MPTR(), HTX_SL_REQ_UPTR() and HTX_SL_REQ_VPTR() are macros to
      get info about a request start-line. These macros only wrap HTX_SL_P*
      ones.

    - HTX_SL_RES_VLEN(), HTX_SL_RES_CLEN(), HTX_SL_RES_RLEN(),
      HTX_SL_RES_VPTR(), HTX_SL_RES_CPTR() and HTX_SL_RES_RPTR() are macros to
      get info about a response start-line. These macros only wrap HTX_SL_P*
      ones.

    - htx_sl_p1(), htx_sl_p2() and htx_sl_p2() are functions to get the ist
      corresponding to the right part of a start-line.

    - htx_sl_req_meth(), htx_sl_req_uri() and htx_sl_req_vsn() get the ist
      corresponding to the right part of a request start-line.

    - htx_sl_res_vsn(), htx_sl_res_code() and htx_sl_res_reason() get the ist
      corresponding to the right part of a response start-line.


4.5. Iterate on the HTX message

To iterate on an HTX message, the first thing to do is to get the HTX block to
start the loop. There are three special blocks in an HTX message that may be
good candidates to start a loop :

    - the head block. It is the oldest inserted block. Multiplexers always start
      to consume an HTX message from this block. The function htx_get_head()
      returns its position and htx_get_head_blk() returns the blocks itself. In
      addition, the function htx_get_head_type() returns its block's type.

    - the tail block. It is the newest inserted block. The function
      htx_get_tail() returns its position and htx_get_tail_blk() returns the
      blocks itself. In addition, the function htx_get_tail_type() returns its
      block's type.

    - the first block. It is the block where to (re)start the analyse. It is
      used as start point by HTX analyzers. The function htx_get_first() returns
      its position and htx_get_first_blk() returns the blocks itself. In
      addition, the function htx_get_first_type() returns its block's type.

For all these functions, if the HTX message is empty, -1 is returned for the
block's position, NULL instead of a block and HTX_BLK_UNUSED for its type.

Then to iterate on blocks, foreword or backward :

    - htx_get_prev() and htx_get_next() return, respectively, the position of
      the previous block or the next block, given a specific position. Or -1 if
      an edge is reached.

    - htx_get_prev_blk() and htx_get_next_blk() return, respectively, the
      previous block or the next one, given a specific block. Or NULL if an edge
      is reached.

4.6. Access block content and info

Following functions may be used to retrieve information about a specific HTX
block :

    - htx_get_blk_pos() returns the position of a block. It must be in the HTX
      message.

    - htx_get_blk_ptr() returns a pointer on the payload of a block.

    - htx_get_blk_type() returns the type of a block.

    - htx_get_blksz() returns the payload size of a block

    - htx_get_blk_name() returns the name of a block, only if it is a header or
      a trailer. Otherwise, it returns an empty string.

    - htx_get_blk_value() returns the value of a block, depending on its
      type. For header and trailer blocks, it is the value field. For markers
      (EOH or EOT), an empty string is returned. For other blocks an ist
      pointing on the block payload is returned.

    - htx_is_unique_blk() may be used to know if a block is the only one
      remaining inside an HTX message, excluding unused blocks. This function is
      pretty useful to determine the end of a HTX message, in conjunction with
      HTX_FL_EOM flag.

4.7. Advanced functions

Some more advanced functions may be used to do complex processing on the HTX
message. These functions are used by HTX analyzers or by multiplexers.

    - htx_truncate() removes all blocks after the one containing a specific
      offset relatively to the head block of the HTX message. If the offset is
      inside a DATA block, it is truncated. For all other blocks, the removal
      starts to the next block.

    - htx_drain() tries to remove a specific amount of bytes of payload. If the
      tail block is a DATA block, it may be truncated if necessary. All other
      block are removed at once or kept. This function returns a mixed value,
      with the first block not removed, or NULL if everything was removed, and
      the amount of data drained.

    - htx_xfer_blks() transfers HTX blocks from an HTX message to another,
      stopping on the first block of a specified type or when a specific amount
      of bytes, including meta-data, was moved. If the tail block is a DATA
      block, it may be partially moved. All other block are transferred at once
      or kept. This function returns a mixed value, with the last block moved,
      or NULL if nothing was moved, and the amount of data transferred. When
      HEADERS or TRAILERS blocks must be transferred, this function transfers
      all of them. Otherwise, if it is not possible, it triggers an error. It is
      the caller responsibility to transfer all headers or trailers at once.

    - htx_append_msg() append an HTX message to another one. All the message is
      copied or nothing. So, if an error occurred, a rollback is performed. This
      function returns 1 on success and 0 on error.

    - htx_reserve_max_data() Reserves the maximum possible size for an HTX data
      block, by extending an existing one or by creating a new one. It returns a
      compound result with the HTX block and the position where new data must be
      inserted (0 for a new block). If an error occurs or if there is no space
      left, NULL is returned instead of a pointer on an HTX block.

    - htx_find_offset() looks for the HTX block containing a specific offset,
      starting at the HTX message's head. The function returns the found HTX
      block and the position inside this block where the offset is. If the
      offset is outside of the HTX message, NULL is returned.

    - htx_defrag() defragments an HTX message. It removes unused blocks and
      unwraps the payloads part. A temporary buffer is used to do so. This
      function never fails. A referenced block may be provided. If so, the
      corresponding new block is returned. Otherwise, NULL is returned.