HPACKEncoder.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. #if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2
  2. using BestHTTP.Extensions;
  3. using BestHTTP.PlatformSupport.Memory;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.IO;
  7. using System.Linq;
  8. namespace BestHTTP.Connections.HTTP2
  9. {
  10. public sealed class HPACKEncoder
  11. {
  12. private HTTP2SettingsManager settingsRegistry;
  13. // https://http2.github.io/http2-spec/compression.html#encoding.context
  14. // When used for bidirectional communication, such as in HTTP, the encoding and decoding dynamic tables
  15. // maintained by an endpoint are completely independent, i.e., the request and response dynamic tables are separate.
  16. private HeaderTable requestTable;
  17. private HeaderTable responseTable;
  18. private HTTP2Handler parent;
  19. public HPACKEncoder(HTTP2Handler parentHandler, HTTP2SettingsManager registry)
  20. {
  21. this.parent = parentHandler;
  22. this.settingsRegistry = registry;
  23. // I'm unsure what settings (local or remote) we should use for these two tables!
  24. this.requestTable = new HeaderTable(this.settingsRegistry.MySettings);
  25. this.responseTable = new HeaderTable(this.settingsRegistry.RemoteSettings);
  26. }
  27. public void Encode(HTTP2Stream context, HTTPRequest request, Queue<HTTP2FrameHeaderAndPayload> to, UInt32 streamId)
  28. {
  29. // Add usage of SETTINGS_MAX_HEADER_LIST_SIZE to be able to create a header and one or more continuation fragments
  30. // (https://httpwg.org/specs/rfc7540.html#SettingValues)
  31. using (BufferPoolMemoryStream bufferStream = new BufferPoolMemoryStream())
  32. {
  33. WriteHeader(bufferStream, ":method", HTTPRequest.MethodNames[(int)request.MethodType]);
  34. // add path
  35. WriteHeader(bufferStream, ":path", request.CurrentUri.PathAndQuery);
  36. // add authority
  37. WriteHeader(bufferStream, ":authority", request.CurrentUri.Authority);
  38. // add scheme
  39. WriteHeader(bufferStream, ":scheme", "https");
  40. //bool hasBody = false;
  41. // add other, regular headers
  42. request.EnumerateHeaders((header, values) =>
  43. {
  44. if (header.Equals("connection", StringComparison.OrdinalIgnoreCase) ||
  45. header.Equals("te", StringComparison.OrdinalIgnoreCase) ||
  46. header.Equals("host", StringComparison.OrdinalIgnoreCase) ||
  47. header.Equals("keep-alive", StringComparison.OrdinalIgnoreCase) ||
  48. header.StartsWith("proxy-", StringComparison.OrdinalIgnoreCase))
  49. return;
  50. //if (!hasBody)
  51. // hasBody = header.Equals("content-length", StringComparison.OrdinalIgnoreCase) && int.Parse(values[0]) > 0;
  52. // https://httpwg.org/specs/rfc7540.html#HttpSequence
  53. // The chunked transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2.
  54. if (header.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase))
  55. {
  56. // error!
  57. return;
  58. }
  59. // https://httpwg.org/specs/rfc7540.html#HttpHeaders
  60. // Just as in HTTP/1.x, header field names are strings of ASCII characters that are compared in a case-insensitive fashion.
  61. // However, header field names MUST be converted to lowercase prior to their encoding in HTTP/2.
  62. // A request or response containing uppercase header field names MUST be treated as malformed
  63. if (header.Any(Char.IsUpper))
  64. header = header.ToLower();
  65. for (int i = 0; i < values.Count; ++i)
  66. {
  67. WriteHeader(bufferStream, header, values[i]);
  68. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  69. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] - Encode - Header({1}/{2}): '{3}': '{4}'", context.Id, i + 1, values.Count, header, values[i]), this.parent.Context, context.Context, request.Context);
  70. }
  71. }, true);
  72. var upStreamInfo = request.GetUpStream();
  73. CreateHeaderFrames(to,
  74. streamId,
  75. bufferStream.ToArray(true),
  76. (UInt32)bufferStream.Length,
  77. upStreamInfo.Stream != null);
  78. }
  79. }
  80. public void Decode(HTTP2Stream context, Stream stream, List<KeyValuePair<string, string>> to)
  81. {
  82. int headerType = stream.ReadByte();
  83. while (headerType != -1)
  84. {
  85. byte firstDataByte = (byte)headerType;
  86. // https://http2.github.io/http2-spec/compression.html#indexed.header.representation
  87. if (BufferHelper.ReadBit(firstDataByte, 0) == 1)
  88. {
  89. var header = ReadIndexedHeader(firstDataByte, stream);
  90. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  91. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - IndexedHeader: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context);
  92. to.Add(header);
  93. }
  94. else if (BufferHelper.ReadValue(firstDataByte, 0, 1) == 1)
  95. {
  96. // https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
  97. if (BufferHelper.ReadValue(firstDataByte, 2, 7) == 0)
  98. {
  99. // Literal Header Field with Incremental Indexing — New Name
  100. var header = ReadLiteralHeaderFieldWithIncrementalIndexing_NewName(firstDataByte, stream);
  101. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  102. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldWithIncrementalIndexing_NewName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context);
  103. this.responseTable.Add(header);
  104. to.Add(header);
  105. }
  106. else
  107. {
  108. // Literal Header Field with Incremental Indexing — Indexed Name
  109. var header = ReadLiteralHeaderFieldWithIncrementalIndexing_IndexedName(firstDataByte, stream);
  110. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  111. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldWithIncrementalIndexing_IndexedName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context);
  112. this.responseTable.Add(header);
  113. to.Add(header);
  114. }
  115. } else if (BufferHelper.ReadValue(firstDataByte, 0, 3) == 0)
  116. {
  117. // https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
  118. if (BufferHelper.ReadValue(firstDataByte, 4, 7) == 0)
  119. {
  120. // Literal Header Field without Indexing — New Name
  121. var header = ReadLiteralHeaderFieldwithoutIndexing_NewName(firstDataByte, stream);
  122. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  123. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldwithoutIndexing_NewName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context);
  124. to.Add(header);
  125. }
  126. else
  127. {
  128. // Literal Header Field without Indexing — Indexed Name
  129. var header = ReadLiteralHeaderFieldwithoutIndexing_IndexedName(firstDataByte, stream);
  130. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  131. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldwithoutIndexing_IndexedName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context);
  132. to.Add(header);
  133. }
  134. }
  135. else if (BufferHelper.ReadValue(firstDataByte, 0, 3) == 1)
  136. {
  137. // https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
  138. if (BufferHelper.ReadValue(firstDataByte, 4, 7) == 0)
  139. {
  140. // Literal Header Field Never Indexed — New Name
  141. var header = ReadLiteralHeaderFieldNeverIndexed_NewName(firstDataByte, stream);
  142. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  143. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldNeverIndexed_NewName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context);
  144. to.Add(header);
  145. }
  146. else
  147. {
  148. // Literal Header Field Never Indexed — Indexed Name
  149. var header = ReadLiteralHeaderFieldNeverIndexed_IndexedName(firstDataByte, stream);
  150. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  151. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldNeverIndexed_IndexedName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context);
  152. to.Add(header);
  153. }
  154. }
  155. else if (BufferHelper.ReadValue(firstDataByte, 0, 2) == 1)
  156. {
  157. // https://http2.github.io/http2-spec/compression.html#encoding.context.update
  158. UInt32 newMaxSize = DecodeInteger(5, firstDataByte, stream);
  159. if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
  160. HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - Dynamic Table Size Update: {1}", context.Id, newMaxSize), this.parent.Context, context.Context, context.AssignedRequest.Context);
  161. //this.settingsRegistry[HTTP2Settings.HEADER_TABLE_SIZE] = (UInt16)newMaxSize;
  162. this.responseTable.MaxDynamicTableSize = (UInt16)newMaxSize;
  163. }
  164. else
  165. {
  166. // ERROR
  167. }
  168. headerType = stream.ReadByte();
  169. }
  170. }
  171. private KeyValuePair<string, string> ReadIndexedHeader(byte firstByte, Stream stream)
  172. {
  173. // https://http2.github.io/http2-spec/compression.html#indexed.header.representation
  174. UInt32 index = DecodeInteger(7, firstByte, stream);
  175. return this.responseTable.GetHeader(index);
  176. }
  177. private KeyValuePair<string, string> ReadLiteralHeaderFieldWithIncrementalIndexing_IndexedName(byte firstByte, Stream stream)
  178. {
  179. // https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
  180. UInt32 keyIndex = DecodeInteger(6, firstByte, stream);
  181. string header = this.responseTable.GetKey(keyIndex);
  182. string value = DecodeString(stream);
  183. return new KeyValuePair<string, string>(header, value);
  184. }
  185. private KeyValuePair<string, string> ReadLiteralHeaderFieldWithIncrementalIndexing_NewName(byte firstByte, Stream stream)
  186. {
  187. // https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
  188. string header = DecodeString(stream);
  189. string value = DecodeString(stream);
  190. return new KeyValuePair<string, string>(header, value);
  191. }
  192. private KeyValuePair<string, string> ReadLiteralHeaderFieldwithoutIndexing_IndexedName(byte firstByte, Stream stream)
  193. {
  194. // https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
  195. UInt32 index = DecodeInteger(4, firstByte, stream);
  196. string header = this.responseTable.GetKey(index);
  197. string value = DecodeString(stream);
  198. return new KeyValuePair<string, string>(header, value);
  199. }
  200. private KeyValuePair<string, string> ReadLiteralHeaderFieldwithoutIndexing_NewName(byte firstByte, Stream stream)
  201. {
  202. // https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
  203. string header = DecodeString(stream);
  204. string value = DecodeString(stream);
  205. return new KeyValuePair<string, string>(header, value);
  206. }
  207. private KeyValuePair<string, string> ReadLiteralHeaderFieldNeverIndexed_IndexedName(byte firstByte, Stream stream)
  208. {
  209. // https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
  210. UInt32 index = DecodeInteger(4, firstByte, stream);
  211. string header = this.responseTable.GetKey(index);
  212. string value = DecodeString(stream);
  213. return new KeyValuePair<string, string>(header, value);
  214. }
  215. private KeyValuePair<string, string> ReadLiteralHeaderFieldNeverIndexed_NewName(byte firstByte, Stream stream)
  216. {
  217. // https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
  218. string header = DecodeString(stream);
  219. string value = DecodeString(stream);
  220. return new KeyValuePair<string, string>(header, value);
  221. }
  222. private string DecodeString(Stream stream)
  223. {
  224. byte start = (byte)stream.ReadByte();
  225. bool rawString = BufferHelper.ReadBit(start, 0) == 0;
  226. UInt32 stringLength = DecodeInteger(7, start, stream);
  227. if (stringLength == 0)
  228. return string.Empty;
  229. if (rawString)
  230. {
  231. byte[] buffer = BufferPool.Get(stringLength, true);
  232. stream.Read(buffer, 0, (int)stringLength);
  233. BufferPool.Release(buffer);
  234. return System.Text.Encoding.UTF8.GetString(buffer, 0, (int)stringLength);
  235. }
  236. else
  237. {
  238. var node = HuffmanEncoder.GetRoot();
  239. byte currentByte = (byte)stream.ReadByte();
  240. byte bitIdx = 0; // 0..7
  241. using (BufferPoolMemoryStream bufferStream = new BufferPoolMemoryStream())
  242. {
  243. do
  244. {
  245. byte bitValue = BufferHelper.ReadBit(currentByte, bitIdx);
  246. if (++bitIdx > 7)
  247. {
  248. stringLength--;
  249. if (stringLength > 0)
  250. {
  251. bitIdx = 0;
  252. currentByte = (byte)stream.ReadByte();
  253. }
  254. }
  255. node = HuffmanEncoder.GetNext(node, bitValue);
  256. if (node.Value != 0)
  257. {
  258. if (node.Value != HuffmanEncoder.EOS)
  259. bufferStream.WriteByte((byte)node.Value);
  260. node = HuffmanEncoder.GetRoot();
  261. }
  262. } while (stringLength > 0);
  263. byte[] buffer = bufferStream.ToArray(true);
  264. string result = System.Text.Encoding.UTF8.GetString(buffer, 0, (int)bufferStream.Length);
  265. BufferPool.Release(buffer);
  266. return result;
  267. }
  268. }
  269. }
  270. private void CreateHeaderFrames(Queue<HTTP2FrameHeaderAndPayload> to, UInt32 streamId, byte[] dataToSend, UInt32 payloadLength, bool hasBody)
  271. {
  272. UInt32 maxFrameSize = this.settingsRegistry.RemoteSettings[HTTP2Settings.MAX_FRAME_SIZE];
  273. // Only one headers frame
  274. if (payloadLength <= maxFrameSize)
  275. {
  276. HTTP2FrameHeaderAndPayload frameHeader = new HTTP2FrameHeaderAndPayload();
  277. frameHeader.Type = HTTP2FrameTypes.HEADERS;
  278. frameHeader.StreamId = streamId;
  279. frameHeader.Flags = (byte)(HTTP2HeadersFlags.END_HEADERS);
  280. if (!hasBody)
  281. frameHeader.Flags |= (byte)(HTTP2HeadersFlags.END_STREAM);
  282. frameHeader.PayloadLength = payloadLength;
  283. frameHeader.Payload = dataToSend;
  284. to.Enqueue(frameHeader);
  285. }
  286. else
  287. {
  288. HTTP2FrameHeaderAndPayload frameHeader = new HTTP2FrameHeaderAndPayload();
  289. frameHeader.Type = HTTP2FrameTypes.HEADERS;
  290. frameHeader.StreamId = streamId;
  291. frameHeader.PayloadLength = maxFrameSize;
  292. frameHeader.Payload = dataToSend;
  293. frameHeader.DontUseMemPool = true;
  294. frameHeader.PayloadOffset = 0;
  295. if (!hasBody)
  296. frameHeader.Flags = (byte)(HTTP2HeadersFlags.END_STREAM);
  297. to.Enqueue(frameHeader);
  298. UInt32 offset = maxFrameSize;
  299. while (offset < payloadLength)
  300. {
  301. frameHeader = new HTTP2FrameHeaderAndPayload();
  302. frameHeader.Type = HTTP2FrameTypes.CONTINUATION;
  303. frameHeader.StreamId = streamId;
  304. frameHeader.PayloadLength = maxFrameSize;
  305. frameHeader.Payload = dataToSend;
  306. frameHeader.PayloadOffset = offset;
  307. offset += maxFrameSize;
  308. if (offset >= payloadLength)
  309. {
  310. frameHeader.Flags = (byte)(HTTP2ContinuationFlags.END_HEADERS);
  311. // last sent continuation fragment will release back the payload buffer
  312. frameHeader.DontUseMemPool = false;
  313. }
  314. else
  315. frameHeader.DontUseMemPool = true;
  316. to.Enqueue(frameHeader);
  317. }
  318. }
  319. }
  320. private void WriteHeader(Stream stream, string header, string value)
  321. {
  322. // https://http2.github.io/http2-spec/compression.html#header.representation
  323. KeyValuePair<UInt32, UInt32> index = this.requestTable.GetIndex(header, value);
  324. if (index.Key == 0 && index.Value == 0)
  325. {
  326. WriteLiteralHeaderFieldWithIncrementalIndexing_NewName(stream, header, value);
  327. this.requestTable.Add(new KeyValuePair<string, string>(header, value));
  328. }
  329. else if (index.Key != 0 && index.Value == 0)
  330. {
  331. WriteLiteralHeaderFieldWithIncrementalIndexing_IndexedName(stream, index.Key, value);
  332. this.requestTable.Add(new KeyValuePair<string, string>(header, value));
  333. }
  334. else
  335. {
  336. WriteIndexedHeaderField(stream, index.Key);
  337. }
  338. }
  339. private static void WriteIndexedHeaderField(Stream stream, UInt32 index)
  340. {
  341. byte requiredBytes = RequiredBytesToEncodeInteger(index, 7);
  342. byte[] buffer = BufferPool.Get(requiredBytes, true);
  343. UInt32 offset = 0;
  344. buffer[0] = 0x80;
  345. EncodeInteger(index, 7, buffer, ref offset);
  346. stream.Write(buffer, 0, (int)offset);
  347. BufferPool.Release(buffer);
  348. }
  349. private static void WriteLiteralHeaderFieldWithIncrementalIndexing_IndexedName(Stream stream, UInt32 index, string value)
  350. {
  351. // https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
  352. UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 6) +
  353. RequiredBytesToEncodeString(value);
  354. byte[] buffer = BufferPool.Get(requiredBytes, true);
  355. UInt32 offset = 0;
  356. buffer[0] = 0x40;
  357. EncodeInteger(index, 6, buffer, ref offset);
  358. EncodeString(value, buffer, ref offset);
  359. stream.Write(buffer, 0, (int)offset);
  360. BufferPool.Release(buffer);
  361. }
  362. private static void WriteLiteralHeaderFieldWithIncrementalIndexing_NewName(Stream stream, string header, string value)
  363. {
  364. // https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
  365. UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value);
  366. byte[] buffer = BufferPool.Get(requiredBytes, true);
  367. UInt32 offset = 0;
  368. buffer[offset++] = 0x40;
  369. EncodeString(header, buffer, ref offset);
  370. EncodeString(value, buffer, ref offset);
  371. stream.Write(buffer, 0, (int)offset);
  372. BufferPool.Release(buffer);
  373. }
  374. private static void WriteLiteralHeaderFieldWithoutIndexing_IndexedName(Stream stream, UInt32 index, string value)
  375. {
  376. // https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
  377. UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 4) + RequiredBytesToEncodeString(value);
  378. byte[] buffer = BufferPool.Get(requiredBytes, true);
  379. UInt32 offset = 0;
  380. buffer[0] = 0;
  381. EncodeInteger(index, 4, buffer, ref offset);
  382. EncodeString(value, buffer, ref offset);
  383. stream.Write(buffer, 0, (int)offset);
  384. BufferPool.Release(buffer);
  385. }
  386. private static void WriteLiteralHeaderFieldWithoutIndexing_NewName(Stream stream, string header, string value)
  387. {
  388. // https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
  389. UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value);
  390. byte[] buffer = BufferPool.Get(requiredBytes, true);
  391. UInt32 offset = 0;
  392. buffer[offset++] = 0;
  393. EncodeString(header, buffer, ref offset);
  394. EncodeString(value, buffer, ref offset);
  395. stream.Write(buffer, 0, (int)offset);
  396. BufferPool.Release(buffer);
  397. }
  398. private static void WriteLiteralHeaderFieldNeverIndexed_IndexedName(Stream stream, UInt32 index, string value)
  399. {
  400. // https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
  401. UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 4) + RequiredBytesToEncodeString(value);
  402. byte[] buffer = BufferPool.Get(requiredBytes, true);
  403. UInt32 offset = 0;
  404. buffer[0] = 0x10;
  405. EncodeInteger(index, 4, buffer, ref offset);
  406. EncodeString(value, buffer, ref offset);
  407. stream.Write(buffer, 0, (int)offset);
  408. BufferPool.Release(buffer);
  409. }
  410. private static void WriteLiteralHeaderFieldNeverIndexed_NewName(Stream stream, string header, string value)
  411. {
  412. // https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
  413. UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value);
  414. byte[] buffer = BufferPool.Get(requiredBytes, true);
  415. UInt32 offset = 0;
  416. buffer[offset++] = 0x10;
  417. EncodeString(header, buffer, ref offset);
  418. EncodeString(value, buffer, ref offset);
  419. stream.Write(buffer, 0, (int)offset);
  420. BufferPool.Release(buffer);
  421. }
  422. private static void WriteDynamicTableSizeUpdate(Stream stream, UInt16 maxSize)
  423. {
  424. // https://http2.github.io/http2-spec/compression.html#encoding.context.update
  425. UInt32 requiredBytes = RequiredBytesToEncodeInteger(maxSize, 5);
  426. byte[] buffer = BufferPool.Get(requiredBytes, true);
  427. UInt32 offset = 0;
  428. buffer[offset] = 0x20;
  429. EncodeInteger(maxSize, 5, buffer, ref offset);
  430. stream.Write(buffer, 0, (int)offset);
  431. BufferPool.Release(buffer);
  432. }
  433. private static UInt32 RequiredBytesToEncodeString(string str)
  434. {
  435. uint requiredBytesForRawStr = RequiredBytesToEncodeRawString(str);
  436. uint requiredBytesForHuffman = RequiredBytesToEncodeStringWithHuffman(str);
  437. requiredBytesForHuffman += RequiredBytesToEncodeInteger(requiredBytesForHuffman, 7);
  438. return Math.Min(requiredBytesForRawStr, requiredBytesForHuffman);
  439. }
  440. private static void EncodeString(string str, byte[] buffer, ref UInt32 offset)
  441. {
  442. uint requiredBytesForRawStr = RequiredBytesToEncodeRawString(str);
  443. uint requiredBytesForHuffman = RequiredBytesToEncodeStringWithHuffman(str);
  444. // if using huffman encoding would produce the same length, we choose raw encoding instead as it requires
  445. // less CPU cicles
  446. if (requiredBytesForRawStr <= requiredBytesForHuffman + RequiredBytesToEncodeInteger(requiredBytesForHuffman, 7))
  447. EncodeRawStringTo(str, buffer, ref offset);
  448. else
  449. EncodeStringWithHuffman(str, requiredBytesForHuffman, buffer, ref offset);
  450. }
  451. // This calculates only the length of the compressed string,
  452. // additional header length must be calculated using the value returned by this function
  453. private static UInt32 RequiredBytesToEncodeStringWithHuffman(string str)
  454. {
  455. int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str);
  456. byte[] strBytes = BufferPool.Get(requiredBytesForStr, true);
  457. System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, strBytes, 0);
  458. UInt32 requiredBits = 0;
  459. for (int i = 0; i < requiredBytesForStr; ++i)
  460. requiredBits += HuffmanEncoder.GetEntryForCodePoint(strBytes[i]).Bits;
  461. BufferPool.Release(strBytes);
  462. return (UInt32)((requiredBits / 8) + ((requiredBits % 8) == 0 ? 0 : 1));
  463. }
  464. private static void EncodeStringWithHuffman(string str, UInt32 encodedLength, byte[] buffer, ref UInt32 offset)
  465. {
  466. int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str);
  467. byte[] strBytes = BufferPool.Get(requiredBytesForStr, true);
  468. System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, strBytes, 0);
  469. // 0. bit: huffman flag
  470. buffer[offset] = 0x80;
  471. // 1..7+ bit: length
  472. EncodeInteger(encodedLength, 7, buffer, ref offset);
  473. byte bufferBitIdx = 0;
  474. for (int i = 0; i < requiredBytesForStr; ++i)
  475. AddCodePointToBuffer(HuffmanEncoder.GetEntryForCodePoint(strBytes[i]), buffer, ref offset, ref bufferBitIdx);
  476. // https://http2.github.io/http2-spec/compression.html#string.literal.representation
  477. // As the Huffman-encoded data doesn't always end at an octet boundary, some padding is inserted after it,
  478. // up to the next octet boundary. To prevent this padding from being misinterpreted as part of the string literal,
  479. // the most significant bits of the code corresponding to the EOS (end-of-string) symbol are used.
  480. if (bufferBitIdx != 0)
  481. AddCodePointToBuffer(HuffmanEncoder.GetEntryForCodePoint(256), buffer, ref offset, ref bufferBitIdx, true);
  482. BufferPool.Release(strBytes);
  483. }
  484. private static void AddCodePointToBuffer(HuffmanEncoder.TableEntry code, byte[] buffer, ref UInt32 offset, ref byte bufferBitIdx, bool finishOnBoundary = false)
  485. {
  486. for (byte codeBitIdx = 1; codeBitIdx <= code.Bits; ++codeBitIdx)
  487. {
  488. byte bit = code.GetBitAtIdx(codeBitIdx);
  489. buffer[offset] = BufferHelper.SetBit(buffer[offset], bufferBitIdx, bit);
  490. // octet boundary reached, proceed to the next octet
  491. if (++bufferBitIdx == 8)
  492. {
  493. if (++offset < buffer.Length)
  494. buffer[offset] = 0;
  495. if (finishOnBoundary)
  496. return;
  497. bufferBitIdx = 0;
  498. }
  499. }
  500. }
  501. private static UInt32 RequiredBytesToEncodeRawString(string str)
  502. {
  503. int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str);
  504. int requiredBytesForLengthPrefix = RequiredBytesToEncodeInteger((UInt32)requiredBytesForStr, 7);
  505. return (UInt32)(requiredBytesForStr + requiredBytesForLengthPrefix);
  506. }
  507. // This method encodes a string without huffman encoding
  508. private static void EncodeRawStringTo(string str, byte[] buffer, ref UInt32 offset)
  509. {
  510. uint requiredBytesForStr = (uint)System.Text.Encoding.UTF8.GetByteCount(str);
  511. int requiredBytesForLengthPrefix = RequiredBytesToEncodeInteger((UInt32)requiredBytesForStr, 7);
  512. UInt32 originalOffset = offset;
  513. buffer[offset] = 0;
  514. EncodeInteger(requiredBytesForStr, 7, buffer, ref offset);
  515. // Zero out the huffman flag
  516. buffer[originalOffset] = BufferHelper.SetBit(buffer[originalOffset], 0, false);
  517. if (offset != originalOffset + requiredBytesForLengthPrefix)
  518. throw new Exception(string.Format("offset({0}) != originalOffset({1}) + requiredBytesForLengthPrefix({1})", offset, originalOffset, requiredBytesForLengthPrefix));
  519. System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, (int)offset);
  520. offset += requiredBytesForStr;
  521. }
  522. private static byte RequiredBytesToEncodeInteger(UInt32 value, byte N)
  523. {
  524. UInt32 maxValue = (1u << N) - 1;
  525. byte count = 0;
  526. // If the integer value is small enough, i.e., strictly less than 2^N-1, it is encoded within the N-bit prefix.
  527. if (value < maxValue)
  528. {
  529. count++;
  530. }
  531. else
  532. {
  533. // Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2^N-1
  534. count++;
  535. value -= maxValue;
  536. while (value >= 0x80)
  537. {
  538. // The most significant bit of each octet is used as a continuation flag: its value is set to 1 except for the last octet in the list.
  539. count++;
  540. value = value / 0x80;
  541. }
  542. count++;
  543. }
  544. return count;
  545. }
  546. // https://http2.github.io/http2-spec/compression.html#integer.representation
  547. private static void EncodeInteger(UInt32 value, byte N, byte[] buffer, ref UInt32 offset)
  548. {
  549. // 2^N - 1
  550. UInt32 maxValue = (1u << N) - 1;
  551. // If the integer value is small enough, i.e., strictly less than 2^N-1, it is encoded within the N-bit prefix.
  552. if (value < maxValue)
  553. {
  554. buffer[offset++] |= (byte)value;
  555. }
  556. else
  557. {
  558. // Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2^N-1
  559. buffer[offset++] |= (byte)(0xFF >> (8 - N));
  560. value -= maxValue;
  561. while (value >= 0x80)
  562. {
  563. // The most significant bit of each octet is used as a continuation flag: its value is set to 1 except for the last octet in the list.
  564. buffer[offset++] = (byte)(0x80 | (0x7F & value));
  565. value = value / 0x80;
  566. }
  567. buffer[offset++] = (byte)value;
  568. }
  569. }
  570. // https://http2.github.io/http2-spec/compression.html#integer.representation
  571. private static UInt32 DecodeInteger(byte N, byte[] buffer, ref UInt32 offset)
  572. {
  573. // The starting value is the value behind the mask of the N bits
  574. UInt32 value = (UInt32)(buffer[offset++] & (byte)(0xFF >> (8 - N)));
  575. // All N bits are 1s ? If so, we have at least one another byte to decode
  576. if (value == (1u << N) - 1)
  577. {
  578. byte shift = 0;
  579. do
  580. {
  581. // The most significant bit is a continuation flag, so we have to mask it out
  582. value += (UInt32)((buffer[offset] & 0x7F) << shift);
  583. shift += 7;
  584. } while ((buffer[offset++] & 0x80) == 0x80);
  585. }
  586. return value;
  587. }
  588. // https://http2.github.io/http2-spec/compression.html#integer.representation
  589. private static UInt32 DecodeInteger(byte N, byte data, Stream stream)
  590. {
  591. // The starting value is the value behind the mask of the N bits
  592. UInt32 value = (UInt32)(data & (byte)(0xFF >> (8 - N)));
  593. // All N bits are 1s ? If so, we have at least one another byte to decode
  594. if (value == (1u << N) - 1)
  595. {
  596. byte shift = 0;
  597. do
  598. {
  599. data = (byte)stream.ReadByte();
  600. // The most significant bit is a continuation flag, so we have to mask it out
  601. value += (UInt32)((data & 0x7F) << shift);
  602. shift += 7;
  603. } while ((data & 0x80) == 0x80);
  604. }
  605. return value;
  606. }
  607. public override string ToString()
  608. {
  609. return this.requestTable.ToString() + this.responseTable.ToString();
  610. }
  611. }
  612. }
  613. #endif