-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathQpackCoder.cs
More file actions
282 lines (220 loc) · 9.8 KB
/
Copy pathQpackCoder.cs
File metadata and controls
282 lines (220 loc) · 9.8 KB
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
using System.Buffers;
using System.Text;
using Shiny.Net.HttpServer.Http2.Hpack;
namespace Shiny.Net.HttpServer.Http3.Qpack;
public sealed class QpackException(string message) : Exception(message);
/// <summary>
/// QPACK prefixed integers (RFC 9204 §4.1.1) — the same scheme HPACK uses.
/// <para>
/// A value that fits in the N low bits of the first byte is written there; otherwise those bits are
/// all set and the remainder follows as a continuation of 7-bit groups.
/// </para>
/// </summary>
static class QpackInteger
{
public static bool TryDecode(ReadOnlySpan<byte> source, int prefixBits, out long value, out int consumed)
{
value = 0;
consumed = 0;
if (source.IsEmpty)
return false;
var mask = (1 << prefixBits) - 1;
value = source[0] & mask;
consumed = 1;
if (value < mask)
return true;
var shift = 0;
while (consumed < source.Length)
{
var b = source[consumed++];
value += (long)(b & 0x7F) << shift;
if ((b & 0x80) == 0)
return true;
shift += 7;
// 62 bits is the widest anything in this protocol can be; more than that is a peer
// trying to make the decoder allocate or loop.
if (shift > 62)
throw new QpackException("An integer is longer than the encoding allows.");
}
return false;
}
public static void Encode(IBufferWriter<byte> writer, long value, int prefixBits, byte prefixPattern)
{
var mask = (1 << prefixBits) - 1;
if (value < mask)
{
var span = writer.GetSpan(1);
span[0] = (byte)(prefixPattern | (byte)value);
writer.Advance(1);
return;
}
var buffer = writer.GetSpan(10);
buffer[0] = (byte)(prefixPattern | (byte)mask);
var written = 1;
var remaining = value - mask;
while (remaining >= 0x80)
{
buffer[written++] = (byte)((remaining & 0x7F) | 0x80);
remaining >>= 7;
}
buffer[written++] = (byte)remaining;
writer.Advance(written);
}
}
/// <summary>
/// Decodes a QPACK field section.
/// <para>
/// The server announces a dynamic table capacity of zero, so a peer may only reference the static
/// table or send literals. That is spec-legal and deliberate: the dynamic table is what makes QPACK
/// complicated — it requires an encoder stream, insert-count tracking and the ability to block a
/// request stream until the table catches up. Refusing it costs a few bytes per response and
/// removes the entire class of head-of-line bugs that come with it.
/// </para>
/// </summary>
sealed class QpackDecoder(int maxFieldSectionSize = 32 * 1024)
{
public List<HeaderField> Decode(ReadOnlySpan<byte> payload)
{
var fields = new List<HeaderField>(16);
var offset = 0;
var totalSize = 0;
// Field section prefix: required insert count, then a signed delta base. Both are zero
// while the dynamic table is unused, but they are always present and must be consumed.
if (!QpackInteger.TryDecode(payload, 8, out var requiredInsertCount, out var consumed))
throw new QpackException("The field section prefix is truncated.");
offset += consumed;
if (requiredInsertCount != 0)
throw new QpackException(
"The peer referenced the dynamic table, which this server declined by announcing a capacity of zero."
);
if (offset >= payload.Length || !QpackInteger.TryDecode(payload[offset..], 7, out _, out consumed))
throw new QpackException("The field section prefix is truncated.");
offset += consumed;
while (offset < payload.Length)
{
var first = payload[offset];
HeaderField field;
if ((first & 0x80) != 0)
{
// 1Txxxxxx — indexed field line. T set means the static table.
if (!QpackInteger.TryDecode(payload[offset..], 6, out var index, out consumed))
throw new QpackException("An indexed field line is truncated.");
offset += consumed;
if ((first & 0x40) == 0)
throw new QpackException("A dynamic table reference is not accepted.");
if (!QpackStaticTable.TryGet(index, out field))
throw new QpackException($"Static table index {index} does not exist.");
}
else if ((first & 0x40) != 0)
{
// 01NTxxxx — literal with a name reference.
if (!QpackInteger.TryDecode(payload[offset..], 4, out var index, out consumed))
throw new QpackException("A literal field line is truncated.");
offset += consumed;
if ((first & 0x10) == 0)
throw new QpackException("A dynamic table name reference is not accepted.");
if (!QpackStaticTable.TryGet(index, out var nameEntry))
throw new QpackException($"Static table index {index} does not exist.");
var value = ReadString(payload, ref offset, prefixBits: 7);
field = new HeaderField(nameEntry.Name, value);
}
else if ((first & 0x20) != 0)
{
// 001Nxxxx — literal with a literal name.
var name = ReadString(payload, ref offset, prefixBits: 3);
var value = ReadString(payload, ref offset, prefixBits: 7);
field = new HeaderField(name, value);
}
else
{
// 0001xxxx and 0000xxxx are the post-base forms, which only exist to reference the
// dynamic table.
throw new QpackException("A post-base field line is not accepted without a dynamic table.");
}
totalSize += field.Name.Length + field.Value.Length + 32;
if (totalSize > maxFieldSectionSize)
throw new QpackException("The field section is larger than the configured limit.");
fields.Add(field);
}
return fields;
}
static string ReadString(ReadOnlySpan<byte> payload, ref int offset, int prefixBits)
{
if (offset >= payload.Length)
throw new QpackException("A string literal is truncated.");
var huffman = (payload[offset] & (1 << prefixBits)) != 0;
if (!QpackInteger.TryDecode(payload[offset..], prefixBits, out var length, out var consumed))
throw new QpackException("A string length is truncated.");
offset += consumed;
if (length < 0 || offset + length > payload.Length)
throw new QpackException("A string literal runs past the end of the field section.");
var bytes = payload.Slice(offset, (int)length);
offset += (int)length;
if (!huffman)
return Encoding.Latin1.GetString(bytes);
// Same Huffman code as HPACK (RFC 7541 Appendix B); QPACK reuses it unchanged.
var buffer = new byte[HpackHuffman.GetMaxDecodedLength(bytes.Length)];
var decoded = HpackHuffman.Decode(bytes, buffer);
return Encoding.Latin1.GetString(buffer.AsSpan(0, decoded));
}
}
/// <summary>
/// Encodes a QPACK field section.
/// <para>
/// Static references where they exist, literals otherwise, and never an insertion — so the encoder
/// stream stays empty and no response can ever be blocked waiting for the peer's table to catch up.
/// </para>
/// </summary>
sealed class QpackEncoder
{
public void Encode(IBufferWriter<byte> writer, IReadOnlyList<HeaderField> fields)
{
ArgumentNullException.ThrowIfNull(fields);
// Required insert count 0 and base delta 0: this section references no dynamic entries, so
// the decoder never has to wait for one.
var prefix = writer.GetSpan(2);
prefix[0] = 0;
prefix[1] = 0;
writer.Advance(2);
foreach (var field in fields)
{
var name = field.Name.ToLowerInvariant();
var exact = QpackStaticTable.FindExact(name, field.Value);
if (exact >= 0)
{
// 1 1 index(6+) — indexed field line, static table.
QpackInteger.Encode(writer, exact, 6, 0xC0);
continue;
}
var byName = QpackStaticTable.FindName(name);
if (byName >= 0)
{
// 0 1 N=0 T=1 index(4+) — literal with a static name reference.
QpackInteger.Encode(writer, byName, 4, 0x50);
WriteString(writer, field.Value, prefixBits: 7);
continue;
}
// 0 0 1 N=0 H index(3+) — literal with a literal name.
WriteString(writer, name, prefixBits: 3, prefixPattern: 0x20);
WriteString(writer, field.Value, prefixBits: 7);
}
}
static void WriteString(IBufferWriter<byte> writer, string value, int prefixBits, byte prefixPattern = 0)
{
var raw = Encoding.Latin1.GetBytes(value);
var huffmanLength = HpackHuffman.GetEncodedLength(raw);
// Huffman only when it actually helps. On short values it routinely does not, and sending
// the longer form would be paying CPU to make the response bigger.
if (huffmanLength < raw.Length)
{
var huffmanFlag = (byte)(prefixPattern | (byte)(1 << prefixBits));
QpackInteger.Encode(writer, huffmanLength, prefixBits, huffmanFlag);
var span = writer.GetSpan(huffmanLength);
var written = HpackHuffman.Encode(raw, span);
writer.Advance(written);
return;
}
QpackInteger.Encode(writer, raw.Length, prefixBits, prefixPattern);
writer.Write(raw);
}
}