HPACKEncoder.cs 34 KB

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