You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
798 lines
34 KiB
798 lines
34 KiB
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2 |
|
|
|
using BestHTTP.Extensions; |
|
using BestHTTP.PlatformSupport.Memory; |
|
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
|
|
namespace BestHTTP.Connections.HTTP2 |
|
{ |
|
public sealed class HPACKEncoder |
|
{ |
|
private HTTP2SettingsManager settingsRegistry; |
|
|
|
// https://http2.github.io/http2-spec/compression.html#encoding.context |
|
// When used for bidirectional communication, such as in HTTP, the encoding and decoding dynamic tables |
|
// maintained by an endpoint are completely independent, i.e., the request and response dynamic tables are separate. |
|
private HeaderTable requestTable; |
|
private HeaderTable responseTable; |
|
|
|
private HTTP2Handler parent; |
|
|
|
public HPACKEncoder(HTTP2Handler parentHandler, HTTP2SettingsManager registry) |
|
{ |
|
this.parent = parentHandler; |
|
this.settingsRegistry = registry; |
|
|
|
// I'm unsure what settings (local or remote) we should use for these two tables! |
|
this.requestTable = new HeaderTable(this.settingsRegistry.MySettings); |
|
this.responseTable = new HeaderTable(this.settingsRegistry.RemoteSettings); |
|
} |
|
|
|
public void Encode(HTTP2Stream context, HTTPRequest request, Queue<HTTP2FrameHeaderAndPayload> to, UInt32 streamId) |
|
{ |
|
// Add usage of SETTINGS_MAX_HEADER_LIST_SIZE to be able to create a header and one or more continuation fragments |
|
// (https://httpwg.org/specs/rfc7540.html#SettingValues) |
|
|
|
using (BufferPoolMemoryStream bufferStream = new BufferPoolMemoryStream()) |
|
{ |
|
WriteHeader(bufferStream, ":method", HTTPRequest.MethodNames[(int)request.MethodType]); |
|
// add path |
|
WriteHeader(bufferStream, ":path", request.CurrentUri.PathAndQuery); |
|
// add authority |
|
WriteHeader(bufferStream, ":authority", request.CurrentUri.Authority); |
|
// add scheme |
|
WriteHeader(bufferStream, ":scheme", "https"); |
|
|
|
//bool hasBody = false; |
|
|
|
// add other, regular headers |
|
request.EnumerateHeaders((header, values) => |
|
{ |
|
if (header.Equals("connection", StringComparison.OrdinalIgnoreCase) || |
|
header.Equals("te", StringComparison.OrdinalIgnoreCase) || |
|
header.Equals("host", StringComparison.OrdinalIgnoreCase) || |
|
header.Equals("keep-alive", StringComparison.OrdinalIgnoreCase) || |
|
header.StartsWith("proxy-", StringComparison.OrdinalIgnoreCase)) |
|
return; |
|
|
|
//if (!hasBody) |
|
// hasBody = header.Equals("content-length", StringComparison.OrdinalIgnoreCase) && int.Parse(values[0]) > 0; |
|
|
|
// https://httpwg.org/specs/rfc7540.html#HttpSequence |
|
// The chunked transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2. |
|
if (header.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
// error! |
|
return; |
|
} |
|
|
|
// https://httpwg.org/specs/rfc7540.html#HttpHeaders |
|
// Just as in HTTP/1.x, header field names are strings of ASCII characters that are compared in a case-insensitive fashion. |
|
// However, header field names MUST be converted to lowercase prior to their encoding in HTTP/2. |
|
// A request or response containing uppercase header field names MUST be treated as malformed |
|
if (header.Any(Char.IsUpper)) |
|
header = header.ToLower(); |
|
|
|
for (int i = 0; i < values.Count; ++i) |
|
{ |
|
WriteHeader(bufferStream, header, values[i]); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
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); |
|
} |
|
}, true); |
|
|
|
var upStreamInfo = request.GetUpStream(); |
|
CreateHeaderFrames(to, |
|
streamId, |
|
bufferStream.ToArray(true), |
|
(UInt32)bufferStream.Length, |
|
upStreamInfo.Stream != null); |
|
} |
|
} |
|
|
|
public void Decode(HTTP2Stream context, Stream stream, List<KeyValuePair<string, string>> to) |
|
{ |
|
int headerType = stream.ReadByte(); |
|
while (headerType != -1) |
|
{ |
|
byte firstDataByte = (byte)headerType; |
|
|
|
// https://http2.github.io/http2-spec/compression.html#indexed.header.representation |
|
if (BufferHelper.ReadBit(firstDataByte, 0) == 1) |
|
{ |
|
var header = ReadIndexedHeader(firstDataByte, stream); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - IndexedHeader: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context); |
|
|
|
to.Add(header); |
|
} |
|
else if (BufferHelper.ReadValue(firstDataByte, 0, 1) == 1) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing |
|
|
|
if (BufferHelper.ReadValue(firstDataByte, 2, 7) == 0) |
|
{ |
|
// Literal Header Field with Incremental Indexing — New Name |
|
var header = ReadLiteralHeaderFieldWithIncrementalIndexing_NewName(firstDataByte, stream); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldWithIncrementalIndexing_NewName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context); |
|
|
|
this.responseTable.Add(header); |
|
to.Add(header); |
|
} |
|
else |
|
{ |
|
// Literal Header Field with Incremental Indexing — Indexed Name |
|
var header = ReadLiteralHeaderFieldWithIncrementalIndexing_IndexedName(firstDataByte, stream); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldWithIncrementalIndexing_IndexedName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context); |
|
|
|
this.responseTable.Add(header); |
|
to.Add(header); |
|
} |
|
} else if (BufferHelper.ReadValue(firstDataByte, 0, 3) == 0) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing |
|
|
|
if (BufferHelper.ReadValue(firstDataByte, 4, 7) == 0) |
|
{ |
|
// Literal Header Field without Indexing — New Name |
|
var header = ReadLiteralHeaderFieldwithoutIndexing_NewName(firstDataByte, stream); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldwithoutIndexing_NewName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context); |
|
|
|
to.Add(header); |
|
} |
|
else |
|
{ |
|
// Literal Header Field without Indexing — Indexed Name |
|
var header = ReadLiteralHeaderFieldwithoutIndexing_IndexedName(firstDataByte, stream); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldwithoutIndexing_IndexedName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context); |
|
|
|
to.Add(header); |
|
} |
|
} |
|
else if (BufferHelper.ReadValue(firstDataByte, 0, 3) == 1) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed |
|
|
|
if (BufferHelper.ReadValue(firstDataByte, 4, 7) == 0) |
|
{ |
|
// Literal Header Field Never Indexed — New Name |
|
var header = ReadLiteralHeaderFieldNeverIndexed_NewName(firstDataByte, stream); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldNeverIndexed_NewName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context); |
|
|
|
to.Add(header); |
|
} |
|
else |
|
{ |
|
// Literal Header Field Never Indexed — Indexed Name |
|
var header = ReadLiteralHeaderFieldNeverIndexed_IndexedName(firstDataByte, stream); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldNeverIndexed_IndexedName: {1}", context.Id, header.ToString()), this.parent.Context, context.Context, context.AssignedRequest.Context); |
|
|
|
to.Add(header); |
|
} |
|
} |
|
else if (BufferHelper.ReadValue(firstDataByte, 0, 2) == 1) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#encoding.context.update |
|
|
|
UInt32 newMaxSize = DecodeInteger(5, firstDataByte, stream); |
|
|
|
if (HTTPManager.Logger.Level <= Logger.Loglevels.Information) |
|
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - Dynamic Table Size Update: {1}", context.Id, newMaxSize), this.parent.Context, context.Context, context.AssignedRequest.Context); |
|
|
|
//this.settingsRegistry[HTTP2Settings.HEADER_TABLE_SIZE] = (UInt16)newMaxSize; |
|
this.responseTable.MaxDynamicTableSize = (UInt16)newMaxSize; |
|
} |
|
else |
|
{ |
|
// ERROR |
|
} |
|
|
|
headerType = stream.ReadByte(); |
|
} |
|
} |
|
|
|
private KeyValuePair<string, string> ReadIndexedHeader(byte firstByte, Stream stream) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#indexed.header.representation |
|
|
|
UInt32 index = DecodeInteger(7, firstByte, stream); |
|
return this.responseTable.GetHeader(index); |
|
} |
|
|
|
private KeyValuePair<string, string> ReadLiteralHeaderFieldWithIncrementalIndexing_IndexedName(byte firstByte, Stream stream) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing |
|
|
|
UInt32 keyIndex = DecodeInteger(6, firstByte, stream); |
|
|
|
string header = this.responseTable.GetKey(keyIndex); |
|
string value = DecodeString(stream); |
|
|
|
return new KeyValuePair<string, string>(header, value); |
|
} |
|
|
|
private KeyValuePair<string, string> ReadLiteralHeaderFieldWithIncrementalIndexing_NewName(byte firstByte, Stream stream) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing |
|
|
|
string header = DecodeString(stream); |
|
string value = DecodeString(stream); |
|
|
|
return new KeyValuePair<string, string>(header, value); |
|
} |
|
|
|
private KeyValuePair<string, string> ReadLiteralHeaderFieldwithoutIndexing_IndexedName(byte firstByte, Stream stream) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing |
|
|
|
UInt32 index = DecodeInteger(4, firstByte, stream); |
|
string header = this.responseTable.GetKey(index); |
|
string value = DecodeString(stream); |
|
|
|
return new KeyValuePair<string, string>(header, value); |
|
} |
|
|
|
private KeyValuePair<string, string> ReadLiteralHeaderFieldwithoutIndexing_NewName(byte firstByte, Stream stream) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing |
|
|
|
string header = DecodeString(stream); |
|
string value = DecodeString(stream); |
|
|
|
return new KeyValuePair<string, string>(header, value); |
|
} |
|
|
|
private KeyValuePair<string, string> ReadLiteralHeaderFieldNeverIndexed_IndexedName(byte firstByte, Stream stream) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed |
|
|
|
UInt32 index = DecodeInteger(4, firstByte, stream); |
|
string header = this.responseTable.GetKey(index); |
|
string value = DecodeString(stream); |
|
|
|
return new KeyValuePair<string, string>(header, value); |
|
} |
|
|
|
private KeyValuePair<string, string> ReadLiteralHeaderFieldNeverIndexed_NewName(byte firstByte, Stream stream) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed |
|
|
|
string header = DecodeString(stream); |
|
string value = DecodeString(stream); |
|
|
|
return new KeyValuePair<string, string>(header, value); |
|
} |
|
|
|
private string DecodeString(Stream stream) |
|
{ |
|
byte start = (byte)stream.ReadByte(); |
|
bool rawString = BufferHelper.ReadBit(start, 0) == 0; |
|
UInt32 stringLength = DecodeInteger(7, start, stream); |
|
|
|
if (stringLength == 0) |
|
return string.Empty; |
|
|
|
if (rawString) |
|
{ |
|
byte[] buffer = BufferPool.Get(stringLength, true); |
|
|
|
stream.Read(buffer, 0, (int)stringLength); |
|
|
|
BufferPool.Release(buffer); |
|
|
|
return System.Text.Encoding.UTF8.GetString(buffer, 0, (int)stringLength); |
|
} |
|
else |
|
{ |
|
var node = HuffmanEncoder.GetRoot(); |
|
byte currentByte = (byte)stream.ReadByte(); |
|
byte bitIdx = 0; // 0..7 |
|
|
|
using (BufferPoolMemoryStream bufferStream = new BufferPoolMemoryStream()) |
|
{ |
|
do |
|
{ |
|
byte bitValue = BufferHelper.ReadBit(currentByte, bitIdx); |
|
|
|
if (++bitIdx > 7) |
|
{ |
|
stringLength--; |
|
|
|
if (stringLength > 0) |
|
{ |
|
bitIdx = 0; |
|
currentByte = (byte)stream.ReadByte(); |
|
} |
|
} |
|
|
|
node = HuffmanEncoder.GetNext(node, bitValue); |
|
|
|
if (node.Value != 0) |
|
{ |
|
if (node.Value != HuffmanEncoder.EOS) |
|
bufferStream.WriteByte((byte)node.Value); |
|
|
|
node = HuffmanEncoder.GetRoot(); |
|
} |
|
} while (stringLength > 0); |
|
|
|
byte[] buffer = bufferStream.ToArray(true); |
|
|
|
string result = System.Text.Encoding.UTF8.GetString(buffer, 0, (int)bufferStream.Length); |
|
|
|
BufferPool.Release(buffer); |
|
|
|
return result; |
|
} |
|
} |
|
} |
|
|
|
private void CreateHeaderFrames(Queue<HTTP2FrameHeaderAndPayload> to, UInt32 streamId, byte[] dataToSend, UInt32 payloadLength, bool hasBody) |
|
{ |
|
UInt32 maxFrameSize = this.settingsRegistry.RemoteSettings[HTTP2Settings.MAX_FRAME_SIZE]; |
|
|
|
// Only one headers frame |
|
if (payloadLength <= maxFrameSize) |
|
{ |
|
HTTP2FrameHeaderAndPayload frameHeader = new HTTP2FrameHeaderAndPayload(); |
|
frameHeader.Type = HTTP2FrameTypes.HEADERS; |
|
frameHeader.StreamId = streamId; |
|
frameHeader.Flags = (byte)(HTTP2HeadersFlags.END_HEADERS); |
|
|
|
if (!hasBody) |
|
frameHeader.Flags |= (byte)(HTTP2HeadersFlags.END_STREAM); |
|
|
|
frameHeader.PayloadLength = payloadLength; |
|
frameHeader.Payload = dataToSend; |
|
|
|
to.Enqueue(frameHeader); |
|
} |
|
else |
|
{ |
|
HTTP2FrameHeaderAndPayload frameHeader = new HTTP2FrameHeaderAndPayload(); |
|
frameHeader.Type = HTTP2FrameTypes.HEADERS; |
|
frameHeader.StreamId = streamId; |
|
frameHeader.PayloadLength = maxFrameSize; |
|
frameHeader.Payload = dataToSend; |
|
frameHeader.DontUseMemPool = true; |
|
frameHeader.PayloadOffset = 0; |
|
|
|
if (!hasBody) |
|
frameHeader.Flags = (byte)(HTTP2HeadersFlags.END_STREAM); |
|
|
|
to.Enqueue(frameHeader); |
|
|
|
UInt32 offset = maxFrameSize; |
|
while (offset < payloadLength) |
|
{ |
|
frameHeader = new HTTP2FrameHeaderAndPayload(); |
|
frameHeader.Type = HTTP2FrameTypes.CONTINUATION; |
|
frameHeader.StreamId = streamId; |
|
frameHeader.PayloadLength = maxFrameSize; |
|
frameHeader.Payload = dataToSend; |
|
frameHeader.PayloadOffset = offset; |
|
|
|
offset += maxFrameSize; |
|
|
|
if (offset >= payloadLength) |
|
{ |
|
frameHeader.Flags = (byte)(HTTP2ContinuationFlags.END_HEADERS); |
|
// last sent continuation fragment will release back the payload buffer |
|
frameHeader.DontUseMemPool = false; |
|
} |
|
else |
|
frameHeader.DontUseMemPool = true; |
|
|
|
to.Enqueue(frameHeader); |
|
} |
|
} |
|
} |
|
|
|
private void WriteHeader(Stream stream, string header, string value) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#header.representation |
|
|
|
KeyValuePair<UInt32, UInt32> index = this.requestTable.GetIndex(header, value); |
|
|
|
if (index.Key == 0 && index.Value == 0) |
|
{ |
|
WriteLiteralHeaderFieldWithIncrementalIndexing_NewName(stream, header, value); |
|
this.requestTable.Add(new KeyValuePair<string, string>(header, value)); |
|
} |
|
else if (index.Key != 0 && index.Value == 0) |
|
{ |
|
WriteLiteralHeaderFieldWithIncrementalIndexing_IndexedName(stream, index.Key, value); |
|
this.requestTable.Add(new KeyValuePair<string, string>(header, value)); |
|
} |
|
else |
|
{ |
|
WriteIndexedHeaderField(stream, index.Key); |
|
} |
|
} |
|
|
|
private static void WriteIndexedHeaderField(Stream stream, UInt32 index) |
|
{ |
|
byte requiredBytes = RequiredBytesToEncodeInteger(index, 7); |
|
byte[] buffer = BufferPool.Get(requiredBytes, true); |
|
UInt32 offset = 0; |
|
|
|
buffer[0] = 0x80; |
|
EncodeInteger(index, 7, buffer, ref offset); |
|
|
|
stream.Write(buffer, 0, (int)offset); |
|
|
|
BufferPool.Release(buffer); |
|
} |
|
|
|
private static void WriteLiteralHeaderFieldWithIncrementalIndexing_IndexedName(Stream stream, UInt32 index, string value) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing |
|
|
|
UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 6) + |
|
RequiredBytesToEncodeString(value); |
|
|
|
byte[] buffer = BufferPool.Get(requiredBytes, true); |
|
UInt32 offset = 0; |
|
|
|
buffer[0] = 0x40; |
|
EncodeInteger(index, 6, buffer, ref offset); |
|
EncodeString(value, buffer, ref offset); |
|
|
|
stream.Write(buffer, 0, (int)offset); |
|
|
|
BufferPool.Release(buffer); |
|
} |
|
|
|
private static void WriteLiteralHeaderFieldWithIncrementalIndexing_NewName(Stream stream, string header, string value) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing |
|
|
|
UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value); |
|
|
|
byte[] buffer = BufferPool.Get(requiredBytes, true); |
|
UInt32 offset = 0; |
|
|
|
buffer[offset++] = 0x40; |
|
EncodeString(header, buffer, ref offset); |
|
EncodeString(value, buffer, ref offset); |
|
|
|
stream.Write(buffer, 0, (int)offset); |
|
|
|
BufferPool.Release(buffer); |
|
} |
|
|
|
private static void WriteLiteralHeaderFieldWithoutIndexing_IndexedName(Stream stream, UInt32 index, string value) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing |
|
|
|
UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 4) + RequiredBytesToEncodeString(value); |
|
|
|
byte[] buffer = BufferPool.Get(requiredBytes, true); |
|
UInt32 offset = 0; |
|
|
|
buffer[0] = 0; |
|
EncodeInteger(index, 4, buffer, ref offset); |
|
EncodeString(value, buffer, ref offset); |
|
|
|
stream.Write(buffer, 0, (int)offset); |
|
|
|
BufferPool.Release(buffer); |
|
} |
|
|
|
private static void WriteLiteralHeaderFieldWithoutIndexing_NewName(Stream stream, string header, string value) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing |
|
|
|
UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value); |
|
|
|
byte[] buffer = BufferPool.Get(requiredBytes, true); |
|
UInt32 offset = 0; |
|
|
|
buffer[offset++] = 0; |
|
EncodeString(header, buffer, ref offset); |
|
EncodeString(value, buffer, ref offset); |
|
|
|
stream.Write(buffer, 0, (int)offset); |
|
|
|
BufferPool.Release(buffer); |
|
} |
|
|
|
private static void WriteLiteralHeaderFieldNeverIndexed_IndexedName(Stream stream, UInt32 index, string value) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed |
|
|
|
UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 4) + RequiredBytesToEncodeString(value); |
|
|
|
byte[] buffer = BufferPool.Get(requiredBytes, true); |
|
UInt32 offset = 0; |
|
|
|
buffer[0] = 0x10; |
|
EncodeInteger(index, 4, buffer, ref offset); |
|
EncodeString(value, buffer, ref offset); |
|
|
|
stream.Write(buffer, 0, (int)offset); |
|
|
|
BufferPool.Release(buffer); |
|
} |
|
|
|
private static void WriteLiteralHeaderFieldNeverIndexed_NewName(Stream stream, string header, string value) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed |
|
|
|
UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value); |
|
|
|
byte[] buffer = BufferPool.Get(requiredBytes, true); |
|
UInt32 offset = 0; |
|
|
|
buffer[offset++] = 0x10; |
|
EncodeString(header, buffer, ref offset); |
|
EncodeString(value, buffer, ref offset); |
|
|
|
stream.Write(buffer, 0, (int)offset); |
|
|
|
BufferPool.Release(buffer); |
|
} |
|
|
|
private static void WriteDynamicTableSizeUpdate(Stream stream, UInt16 maxSize) |
|
{ |
|
// https://http2.github.io/http2-spec/compression.html#encoding.context.update |
|
|
|
UInt32 requiredBytes = RequiredBytesToEncodeInteger(maxSize, 5); |
|
|
|
byte[] buffer = BufferPool.Get(requiredBytes, true); |
|
UInt32 offset = 0; |
|
|
|
buffer[offset] = 0x20; |
|
EncodeInteger(maxSize, 5, buffer, ref offset); |
|
|
|
stream.Write(buffer, 0, (int)offset); |
|
|
|
BufferPool.Release(buffer); |
|
} |
|
|
|
private static UInt32 RequiredBytesToEncodeString(string str) |
|
{ |
|
uint requiredBytesForRawStr = RequiredBytesToEncodeRawString(str); |
|
uint requiredBytesForHuffman = RequiredBytesToEncodeStringWithHuffman(str); |
|
requiredBytesForHuffman += RequiredBytesToEncodeInteger(requiredBytesForHuffman, 7); |
|
|
|
return Math.Min(requiredBytesForRawStr, requiredBytesForHuffman); |
|
} |
|
|
|
private static void EncodeString(string str, byte[] buffer, ref UInt32 offset) |
|
{ |
|
uint requiredBytesForRawStr = RequiredBytesToEncodeRawString(str); |
|
uint requiredBytesForHuffman = RequiredBytesToEncodeStringWithHuffman(str); |
|
|
|
// if using huffman encoding would produce the same length, we choose raw encoding instead as it requires |
|
// less CPU cicles |
|
if (requiredBytesForRawStr <= requiredBytesForHuffman + RequiredBytesToEncodeInteger(requiredBytesForHuffman, 7)) |
|
EncodeRawStringTo(str, buffer, ref offset); |
|
else |
|
EncodeStringWithHuffman(str, requiredBytesForHuffman, buffer, ref offset); |
|
} |
|
|
|
// This calculates only the length of the compressed string, |
|
// additional header length must be calculated using the value returned by this function |
|
private static UInt32 RequiredBytesToEncodeStringWithHuffman(string str) |
|
{ |
|
int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str); |
|
byte[] strBytes = BufferPool.Get(requiredBytesForStr, true); |
|
|
|
System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, strBytes, 0); |
|
|
|
UInt32 requiredBits = 0; |
|
|
|
for (int i = 0; i < requiredBytesForStr; ++i) |
|
requiredBits += HuffmanEncoder.GetEntryForCodePoint(strBytes[i]).Bits; |
|
|
|
BufferPool.Release(strBytes); |
|
|
|
return (UInt32)((requiredBits / 8) + ((requiredBits % 8) == 0 ? 0 : 1)); |
|
} |
|
|
|
private static void EncodeStringWithHuffman(string str, UInt32 encodedLength, byte[] buffer, ref UInt32 offset) |
|
{ |
|
int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str); |
|
byte[] strBytes = BufferPool.Get(requiredBytesForStr, true); |
|
|
|
System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, strBytes, 0); |
|
|
|
// 0. bit: huffman flag |
|
buffer[offset] = 0x80; |
|
|
|
// 1..7+ bit: length |
|
EncodeInteger(encodedLength, 7, buffer, ref offset); |
|
|
|
byte bufferBitIdx = 0; |
|
|
|
for (int i = 0; i < requiredBytesForStr; ++i) |
|
AddCodePointToBuffer(HuffmanEncoder.GetEntryForCodePoint(strBytes[i]), buffer, ref offset, ref bufferBitIdx); |
|
|
|
// https://http2.github.io/http2-spec/compression.html#string.literal.representation |
|
// As the Huffman-encoded data doesn't always end at an octet boundary, some padding is inserted after it, |
|
// up to the next octet boundary. To prevent this padding from being misinterpreted as part of the string literal, |
|
// the most significant bits of the code corresponding to the EOS (end-of-string) symbol are used. |
|
if (bufferBitIdx != 0) |
|
AddCodePointToBuffer(HuffmanEncoder.GetEntryForCodePoint(256), buffer, ref offset, ref bufferBitIdx, true); |
|
|
|
BufferPool.Release(strBytes); |
|
} |
|
|
|
private static void AddCodePointToBuffer(HuffmanEncoder.TableEntry code, byte[] buffer, ref UInt32 offset, ref byte bufferBitIdx, bool finishOnBoundary = false) |
|
{ |
|
for (byte codeBitIdx = 1; codeBitIdx <= code.Bits; ++codeBitIdx) |
|
{ |
|
byte bit = code.GetBitAtIdx(codeBitIdx); |
|
buffer[offset] = BufferHelper.SetBit(buffer[offset], bufferBitIdx, bit); |
|
|
|
// octet boundary reached, proceed to the next octet |
|
if (++bufferBitIdx == 8) |
|
{ |
|
if (++offset < buffer.Length) |
|
buffer[offset] = 0; |
|
|
|
if (finishOnBoundary) |
|
return; |
|
|
|
bufferBitIdx = 0; |
|
} |
|
} |
|
} |
|
|
|
private static UInt32 RequiredBytesToEncodeRawString(string str) |
|
{ |
|
int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str); |
|
int requiredBytesForLengthPrefix = RequiredBytesToEncodeInteger((UInt32)requiredBytesForStr, 7); |
|
|
|
return (UInt32)(requiredBytesForStr + requiredBytesForLengthPrefix); |
|
} |
|
|
|
// This method encodes a string without huffman encoding |
|
private static void EncodeRawStringTo(string str, byte[] buffer, ref UInt32 offset) |
|
{ |
|
uint requiredBytesForStr = (uint)System.Text.Encoding.UTF8.GetByteCount(str); |
|
int requiredBytesForLengthPrefix = RequiredBytesToEncodeInteger((UInt32)requiredBytesForStr, 7); |
|
|
|
UInt32 originalOffset = offset; |
|
buffer[offset] = 0; |
|
EncodeInteger(requiredBytesForStr, 7, buffer, ref offset); |
|
|
|
// Zero out the huffman flag |
|
buffer[originalOffset] = BufferHelper.SetBit(buffer[originalOffset], 0, false); |
|
|
|
if (offset != originalOffset + requiredBytesForLengthPrefix) |
|
throw new Exception(string.Format("offset({0}) != originalOffset({1}) + requiredBytesForLengthPrefix({1})", offset, originalOffset, requiredBytesForLengthPrefix)); |
|
|
|
System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, (int)offset); |
|
offset += requiredBytesForStr; |
|
} |
|
|
|
private static byte RequiredBytesToEncodeInteger(UInt32 value, byte N) |
|
{ |
|
UInt32 maxValue = (1u << N) - 1; |
|
byte count = 0; |
|
|
|
// If the integer value is small enough, i.e., strictly less than 2^N-1, it is encoded within the N-bit prefix. |
|
if (value < maxValue) |
|
{ |
|
count++; |
|
} |
|
else |
|
{ |
|
// Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2^N-1 |
|
count++; |
|
value -= maxValue; |
|
|
|
while (value >= 0x80) |
|
{ |
|
// 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. |
|
count++; |
|
value = value / 0x80; |
|
} |
|
|
|
count++; |
|
} |
|
|
|
return count; |
|
} |
|
|
|
// https://http2.github.io/http2-spec/compression.html#integer.representation |
|
private static void EncodeInteger(UInt32 value, byte N, byte[] buffer, ref UInt32 offset) |
|
{ |
|
// 2^N - 1 |
|
UInt32 maxValue = (1u << N) - 1; |
|
|
|
// If the integer value is small enough, i.e., strictly less than 2^N-1, it is encoded within the N-bit prefix. |
|
if (value < maxValue) |
|
{ |
|
buffer[offset++] |= (byte)value; |
|
} |
|
else |
|
{ |
|
// Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2^N-1 |
|
buffer[offset++] |= (byte)(0xFF >> (8 - N)); |
|
value -= maxValue; |
|
|
|
while (value >= 0x80) |
|
{ |
|
// 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. |
|
buffer[offset++] = (byte)(0x80 | (0x7F & value)); |
|
value = value / 0x80; |
|
} |
|
|
|
buffer[offset++] = (byte)value; |
|
} |
|
} |
|
|
|
// https://http2.github.io/http2-spec/compression.html#integer.representation |
|
private static UInt32 DecodeInteger(byte N, byte[] buffer, ref UInt32 offset) |
|
{ |
|
// The starting value is the value behind the mask of the N bits |
|
UInt32 value = (UInt32)(buffer[offset++] & (byte)(0xFF >> (8 - N))); |
|
|
|
// All N bits are 1s ? If so, we have at least one another byte to decode |
|
if (value == (1u << N) - 1) |
|
{ |
|
byte shift = 0; |
|
|
|
do |
|
{ |
|
// The most significant bit is a continuation flag, so we have to mask it out |
|
value += (UInt32)((buffer[offset] & 0x7F) << shift); |
|
shift += 7; |
|
} while ((buffer[offset++] & 0x80) == 0x80); |
|
} |
|
|
|
return value; |
|
} |
|
|
|
// https://http2.github.io/http2-spec/compression.html#integer.representation |
|
private static UInt32 DecodeInteger(byte N, byte data, Stream stream) |
|
{ |
|
// The starting value is the value behind the mask of the N bits |
|
UInt32 value = (UInt32)(data & (byte)(0xFF >> (8 - N))); |
|
|
|
// All N bits are 1s ? If so, we have at least one another byte to decode |
|
if (value == (1u << N) - 1) |
|
{ |
|
byte shift = 0; |
|
|
|
do |
|
{ |
|
data = (byte)stream.ReadByte(); |
|
|
|
// The most significant bit is a continuation flag, so we have to mask it out |
|
value += (UInt32)((data & 0x7F) << shift); |
|
shift += 7; |
|
} while ((data & 0x80) == 0x80); |
|
} |
|
|
|
return value; |
|
} |
|
|
|
public override string ToString() |
|
{ |
|
return this.requestTable.ToString() + this.responseTable.ToString(); |
|
} |
|
} |
|
} |
|
|
|
#endif
|
|
|