├── Hpack ├── .gitignore ├── TableEntry.cs ├── Defaults.cs ├── HeaderField.cs ├── Hpack.csproj ├── IntDecoder.cs ├── HeaderTable.cs ├── IntEncoder.cs ├── DynamicTable.cs ├── StaticTable.cs └── StringEncoder.cs ├── Http2 ├── .gitignore ├── Constants.cs ├── Defaults.cs ├── Http2.csproj ├── Internal │ └── Events.cs ├── ByteStreamExtensions.cs ├── FramePrinter.cs ├── ByteStreams.cs ├── HeaderUtils.cs ├── IoStreamExtensions.cs ├── ClientPreface.cs ├── IStream.cs ├── Error.cs ├── ClientUpgradeRequest.cs ├── SocketExtensions.cs └── ConnectionConfiguration.cs ├── HpackTests ├── .gitignore ├── HpackTests.csproj ├── Buffer.cs ├── HeaderTable.cs ├── Huffman.cs ├── IntEncoder.cs ├── StringEncoder.cs ├── StringDecoder.cs └── IntDecoder.cs ├── Http2Tests ├── .gitignore ├── Http2Tests.csproj ├── GeneralTests.cs ├── ByteStreamExtensionsTests.cs ├── ConnectionUnknownFrameTests.cs ├── FrameTests.cs ├── TestHeaders.cs ├── FramePrinterTests.cs ├── BufferStreams.cs ├── ConnectionUtils.cs ├── ConnectionPriorityFrameTests.cs ├── ClientPrefaceTests.cs ├── ConnectionResetStreamTests.cs ├── ConnectionPushPromiseTests.cs ├── ConnectionPrefaceTests.cs ├── HeaderValidationTests.cs ├── ClientUpgradeTests.cs ├── XUnitOutputLogger.cs └── ConnectionCompletionTests.cs ├── .gitignore ├── Examples ├── CliClient │ ├── .gitignore │ ├── readme.md │ └── CliClient.csproj ├── ExampleServer │ ├── .gitignore │ ├── ExampleServer.csproj │ └── Program.cs ├── UpgradeExampleServer │ ├── .gitignore │ └── UpgradeExampleServer.csproj ├── BenchmarkServer │ ├── .gitignore │ ├── BenchmarkServer.csproj │ └── Program.cs ├── Shared │ ├── localhost.p12 │ ├── UpgradeReadStream.cs │ └── Http1Types.cs └── HttpsExampleServer │ ├── HttpsExampleServer.csproj │ └── Program.cs ├── global.json ├── .vscode ├── tasks.json └── launch.json └── LICENSE /Hpack/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /Http2/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /HpackTests/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /Http2Tests/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | .vscode 3 | .vs -------------------------------------------------------------------------------- /Examples/CliClient/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /Examples/ExampleServer/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /Examples/UpgradeExampleServer/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /Examples/BenchmarkServer/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | launchSettings.json -------------------------------------------------------------------------------- /Examples/Shared/localhost.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matthias247/http2dotnet/HEAD/Examples/Shared/localhost.p12 -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | "Hpack", 4 | "Http2", 5 | "HpackTests", 6 | "Http2Tests", 7 | "Examples/ExampleServer", 8 | "Examples/UpgradeExampleServer", 9 | "Examples/BenchmarkServer" 10 | ], 11 | "sdk": { 12 | "version": "1.0.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Hpack/TableEntry.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Http2.Hpack 3 | { 4 | /// 5 | /// An entry in the static or dynamic table 6 | /// 7 | public struct TableEntry 8 | { 9 | public string Name; 10 | public int NameLen; 11 | public string Value; 12 | public int ValueLen; 13 | } 14 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "dotnet", 4 | "isShellCommand": true, 5 | "args": [], 6 | "tasks": [ 7 | { 8 | "taskName": "build", 9 | "args": [ 10 | "" 11 | ], 12 | "isBuildCommand": true, 13 | "problemMatcher": "$msCompile" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /Examples/CliClient/readme.md: -------------------------------------------------------------------------------- 1 | # CliClient 2 | 3 | This example implements a very basic commandline client on top of the HTTP/2 4 | library. It supports fetching making HTTP/2 requests in nghttp style with 5 | various commandline arguments. It also supports a very basic benchmark mode. 6 | 7 | The client supports HTTP/2 connections with prior knowledge as well as HTTP/2 8 | upgrade scenarios. 9 | 10 | For further information start the client without arguments or with `--help` to 11 | get detailed information. -------------------------------------------------------------------------------- /Http2/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Http2 4 | { 5 | /// 6 | /// Contains constant values for HTTP/2 7 | /// 8 | static class Constants 9 | { 10 | /// An empty array segment 11 | public static readonly ArraySegment EmptyByteArray = 12 | new ArraySegment(new byte[0]); 13 | 14 | /// The initial flow control window for connections 15 | public const int InitialConnectionWindowSize = 65535; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /HpackTests/HpackTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Hpack/Defaults.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Http2.Hpack 3 | { 4 | /// 5 | /// Stores default values for the library 6 | /// 7 | static class Defaults 8 | { 9 | /// The default size of the dynamic table: 4096 bytes 10 | public const int DynamicTableSize = 4096; 11 | 12 | /// The default limit for the dynamic table size: 4096 bytes 13 | public const int DynamicTableSizeLimit = 4096; 14 | 15 | /// Maximum length for received strings 16 | public const int MaxStringLength = 1 << 20; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Http2Tests/Http2Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Examples/ExampleServer/ExampleServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Examples/BenchmarkServer/BenchmarkServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Hpack/HeaderField.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Http2.Hpack 3 | { 4 | /// 5 | /// An HTTP/2 header field 6 | /// 7 | public struct HeaderField 8 | { 9 | /// 10 | /// The name of the header field 11 | /// 12 | public string Name; 13 | 14 | /// 15 | /// The value of the header field 16 | /// 17 | public string Value; 18 | 19 | /// 20 | /// Whether the contained information is sensitive 21 | /// and should not be cached. 22 | /// This defaults to false if not set. 23 | /// 24 | public bool Sensitive; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Examples/HttpsExampleServer/HttpsExampleServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 6 | netcoreapp2.1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Examples/UpgradeExampleServer/UpgradeExampleServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Http2/Defaults.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Http2 3 | { 4 | /// 5 | /// Default values for HTTP/2 6 | /// 7 | static class Defaults 8 | { 9 | /// 10 | /// Default value for the maximum encoder HPACK headertable size. 11 | /// HPACK won't use more than this size for the encoder table even if 12 | /// the remote announces a bigger size through Settings. 13 | /// The default size of the dynamic table: 4096 bytes 14 | /// 15 | public const int MaxEncoderHeaderTableSize = 4096; 16 | 17 | /// 18 | /// The timeout until which we expect to get an ACK for our settings 19 | /// 20 | public const int SettingsTimeout = 5000; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Launch (console)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceRoot}/bin/Debug//", 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "externalConsole": false, 13 | "stopAtEntry": false, 14 | "internalConsoleOptions": "openOnSessionStart" 15 | }, 16 | { 17 | "name": ".NET Core Attach", 18 | "type": "coreclr", 19 | "request": "attach", 20 | "processId": "${command.pickProcess}" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /Examples/CliClient/CliClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Hpack/Hpack.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.4 4 | http2dotnet.hpack 5 | 0.8.0 6 | Matthias Einwag 7 | 8 | HTTP/2 HPACK header encoding and decoding implementation for .NET standard 9 | 10 | false 11 | 12 | 13 | https://github.com/Matthias247/http2dotnet 14 | 15 | 16 | https://github.com/Matthias247/http2dotnet 17 | 18 | git 19 | 20 | https://raw.githubusercontent.com/Matthias247/http2dotnet/master/LICENSE 21 | 22 | HTTP HTTP2 HTTP/2 HPACK Network Protocol 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Matthias Einwag 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Http2/Http2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.4 4 | http2dotnet 5 | 0.8.0 6 | Matthias Einwag 7 | HTTP/2 protocol implementation for .NET standard 8 | false 9 | 10 | 11 | https://github.com/Matthias247/http2dotnet 12 | 13 | 14 | https://github.com/Matthias247/http2dotnet 15 | 16 | git 17 | 18 | https://raw.githubusercontent.com/Matthias247/http2dotnet/master/LICENSE 19 | 20 | HTTP HTTP2 HTTP/2 HPACK Network Protocol 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Http2Tests/GeneralTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using Xunit; 6 | 7 | namespace Http2Tests 8 | { 9 | public class GeneralTests 10 | { 11 | [Fact] 12 | public async Task TimersShouldBeCancellable() 13 | { 14 | // This is just a sanity check for the platform, that checks whether 15 | // timers through Task.Delay are cancellable. 16 | var cts = new CancellationTokenSource(); 17 | var sw = new Stopwatch(); 18 | sw.Start(); 19 | var task = Task.Delay(100, cts.Token); 20 | cts.Cancel(); 21 | cts.Dispose(); 22 | try 23 | { 24 | await task; 25 | sw.Stop(); 26 | Assert.False(true, "Cancelled task should not succeed"); 27 | } 28 | catch (TaskCanceledException) 29 | { 30 | sw.Stop(); 31 | } 32 | Assert.True(task.IsCanceled); 33 | // Check how fast the cancellation is processed 34 | // If the timers of the platform are far off this 35 | // might produce errors. 36 | Assert.True(sw.ElapsedMilliseconds < 20); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Http2Tests/ByteStreamExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | using Http2; 5 | using Xunit; 6 | using System.Threading.Tasks; 7 | 8 | namespace Http2Tests 9 | { 10 | public class ByteStreamExtensionTests 11 | { 12 | public class ReadAll 13 | { 14 | [Fact] 15 | public async Task ShouldReadAllRequiredDataFromAConnection() 16 | { 17 | // Read 111 bytes in 3 parts 18 | var source = new BufferReadStream(111, 50); 19 | for (var i = 0; i < 111; i++) source.Buffer[i] = (byte)i; 20 | source.Written = 111; 21 | var dest = new byte[111]; 22 | var destView = new ArraySegment(dest); 23 | await source.ReadAll(destView); 24 | for (var i = 0; i < 111; i++) 25 | { 26 | Assert.Equal(source.Buffer[i], dest[i]); 27 | } 28 | Assert.Equal(3, source.NrReads); 29 | } 30 | 31 | [Fact] 32 | public async Task ShouldYieldAnErrorIfNotEnoughDataIsAvailable() 33 | { 34 | var source = new BufferReadStream(111, 50); 35 | source.Written = 111; 36 | // need 112 bytes, but only 111 are available 37 | var dest = new ArraySegment(new byte[112]); 38 | await Assert.ThrowsAsync( 39 | async () => await source.ReadAll(dest)); 40 | Assert.Equal(4, source.NrReads); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Http2/Internal/Events.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Http2.Internal 7 | { 8 | public class AsyncManualResetEvent : ICriticalNotifyCompletion 9 | { 10 | private static readonly Action isSet = () => { }; 11 | private static readonly Action isReset = () => { }; 12 | 13 | private volatile Action _state = isReset; 14 | 15 | public bool IsCompleted => _state == isSet; 16 | public bool IsReset => _state == isReset; 17 | 18 | public AsyncManualResetEvent(bool signaled) 19 | { 20 | if (signaled) _state = isSet; 21 | } 22 | 23 | public void UnsafeOnCompleted(Action continuation) => OnCompleted(continuation); 24 | 25 | public void OnCompleted(Action continuation) 26 | { 27 | if (continuation == null) return; 28 | 29 | var previous = Interlocked.CompareExchange(ref _state, continuation, isReset); 30 | if (previous == isSet) 31 | { 32 | continuation(); 33 | } 34 | } 35 | 36 | public void GetResult() 37 | { 38 | } 39 | 40 | public void Reset() 41 | { 42 | Interlocked.Exchange(ref _state, isReset); 43 | } 44 | 45 | public void Set() 46 | { 47 | var completion = Interlocked.Exchange(ref _state, isSet); 48 | if (completion != isSet && completion != isReset) 49 | { 50 | Task.Run(completion); 51 | } 52 | } 53 | 54 | public AsyncManualResetEvent GetAwaiter() => this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /HpackTests/Buffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace HpackTests 6 | { 7 | public class Buffer 8 | { 9 | List bytes; 10 | 11 | public Buffer() 12 | { 13 | bytes = new List(); 14 | } 15 | 16 | public void WriteByte(byte b) 17 | { 18 | bytes.Add(b); 19 | } 20 | 21 | public void WriteByte(char b) 22 | { 23 | bytes.Add((byte)b); 24 | } 25 | 26 | public void WriteBytes(byte[] bytes) 27 | { 28 | foreach (var b in bytes) WriteByte(b); 29 | } 30 | 31 | public void WriteString(string s) 32 | { 33 | var bytes = Encoding.ASCII.GetBytes(s); 34 | WriteBytes(bytes); 35 | } 36 | 37 | public void AddHexString(string s) 38 | { 39 | var byteBuf = new byte[2]; 40 | for (int i = 0; i < s.Length; i += 2) 41 | { 42 | WriteByte(Convert.ToByte(s.Substring(i, 2), 16)); 43 | } 44 | } 45 | 46 | public void RemoveFront(int amount) 47 | { 48 | for (var i = 0; i < amount; i++) 49 | { 50 | bytes.RemoveAt(0); 51 | } 52 | } 53 | 54 | public int Length 55 | { 56 | get { return bytes.Count; } 57 | } 58 | 59 | public byte[] Bytes 60 | { 61 | get { return bytes.ToArray(); } 62 | } 63 | 64 | public ArraySegment View 65 | { 66 | get 67 | { 68 | var b = bytes.ToArray(); 69 | return new ArraySegment(b, 0, b.Length); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Http2/ByteStreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Http2 5 | { 6 | /// 7 | /// Utility and extension functions for working with byte streams 8 | /// 9 | public static class ByteStreamExtensions 10 | { 11 | /// 12 | /// Tries to read exactly the given amount of data from a stream. 13 | /// The method will only return if all data was read, the stream 14 | /// closed or the an error happened. 15 | /// If the input is a 0 byte buffer the method will always succeed, 16 | /// even if the underlying stream was already closed. 17 | /// 18 | /// The stream to read data from 19 | /// The destination buffer 20 | /// Awaitable task object 21 | public async static ValueTask ReadAll( 22 | this IReadableByteStream stream, ArraySegment buffer) 23 | { 24 | var array = buffer.Array; 25 | var offset = buffer.Offset; 26 | var count = buffer.Count; 27 | 28 | // Remark: This will not perform actual 0 byte reads to the underlying 29 | // stream, which means it won't detect closed streams on 0 byte reads. 30 | 31 | while (count != 0) 32 | { 33 | var segment = new ArraySegment(array, offset, count); 34 | var res = await stream.ReadAsync(segment); 35 | if (res.EndOfStream) 36 | { 37 | throw new System.IO.EndOfStreamException(); 38 | } 39 | offset += res.BytesRead; 40 | count -= res.BytesRead; 41 | } 42 | 43 | return DoneHandle.Instance; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Http2Tests/ConnectionUnknownFrameTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | using Http2; 9 | 10 | namespace Http2Tests 11 | { 12 | public class ConnectionUnknownFrameTests 13 | { 14 | private readonly ILoggerProvider loggerProvider; 15 | 16 | public ConnectionUnknownFrameTests(ITestOutputHelper outputHelper) 17 | { 18 | this.loggerProvider = new XUnitOutputLoggerProvider(outputHelper); 19 | } 20 | 21 | [Theory] 22 | [InlineData(true, 0)] 23 | [InlineData(true, 512)] 24 | [InlineData(false, 0)] 25 | [InlineData(false, 512)] 26 | public async Task ConnectionShouldIgnoreUnknownFrames( 27 | bool isServer, int payloadLength) 28 | { 29 | var inPipe = new BufferedPipe(1024); 30 | var outPipe = new BufferedPipe(1024); 31 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 32 | isServer, inPipe, outPipe, loggerProvider); 33 | 34 | // send an undefined frame type 35 | var fh = new FrameHeader 36 | { 37 | Type = (FrameType)100, 38 | Flags = 33, 39 | Length = payloadLength, 40 | StreamId = 0, 41 | }; 42 | await inPipe.WriteFrameHeader(fh); 43 | if (payloadLength != 0) 44 | { 45 | var payload = new byte[payloadLength]; 46 | await inPipe.WriteAsync(new ArraySegment(payload)); 47 | } 48 | 49 | // Send a ping afterwards 50 | // If we get a response the unknown frame in between was ignored 51 | var pingData = new byte[8]; 52 | for (var i = 0; i < 8; i++) pingData[i] = (byte)i; 53 | await inPipe.WritePing(pingData, false); 54 | await outPipe.ReadAndDiscardPong(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Http2/FramePrinter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace Http2 5 | { 6 | /// 7 | /// Utilites for printing frames 8 | /// 9 | public static class FramePrinter 10 | { 11 | private static string FrameTypeString(FrameType t) 12 | { 13 | var str = Enum.GetName(typeof(FrameType), t); 14 | if (str == null) return "Unknown"; 15 | return str; 16 | } 17 | 18 | private static void AddFlags(StringBuilder b, FrameHeader h) 19 | { 20 | var flagEnum = FrameFlagsMapping.GetFlagType(h.Type); 21 | if (flagEnum == null) 22 | { 23 | // No enumeration that defines the flags was found 24 | if (h.Flags == 0) return; 25 | b.Append("0x"); 26 | b.Append(h.Flags.ToString("x2")); 27 | return; 28 | } 29 | 30 | var first = true; 31 | foreach (byte flagValue in Enum.GetValues(flagEnum)) 32 | { 33 | if ((flagValue & h.Flags) != 0) 34 | { 35 | // The flag is set 36 | if (!first) b.Append(','); 37 | first = false; 38 | b.Append(Enum.GetName(flagEnum, flagValue)); 39 | } 40 | } 41 | } 42 | 43 | /// 44 | /// Prints the frame header into human readable format 45 | /// 46 | /// The header to print 47 | /// The printed header 48 | public static string PrintFrameHeader(FrameHeader h) 49 | { 50 | var b = new StringBuilder(); 51 | b.Append($"{ FrameTypeString(h.Type)} flags=["); 52 | AddFlags(b, h); 53 | b.Append("] streamId=0x"); 54 | b.Append(h.StreamId.ToString("x8")); 55 | b.Append(" length="); 56 | b.Append(h.Length); 57 | return b.ToString(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Http2Tests/FrameTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Xunit; 4 | 5 | using Http2; 6 | 7 | namespace Http2Tests 8 | { 9 | public class FrameTests 10 | { 11 | [Fact] 12 | public void FrameHeadersShouldBeDecodable() 13 | { 14 | var header = new byte[] { 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF }; 15 | var frame = FrameHeader.DecodeFrom(new ArraySegment(header)); 16 | Assert.Equal((1 << 24) - 1, frame.Length); 17 | Assert.Equal(FrameType.Data, frame.Type); 18 | Assert.Equal(0xFF, frame.Flags); 19 | Assert.Equal(0x7FFFFFFFu, frame.StreamId); 20 | 21 | header = new byte[] { 0x03, 0x40, 0xF1, 0xFF, 0x09, 0x1A, 0xB8, 0x34, 0x12 }; 22 | frame = FrameHeader.DecodeFrom(new ArraySegment(header)); 23 | Assert.Equal(0x0340F1, frame.Length); 24 | Assert.Equal((FrameType)0xFF, frame.Type); 25 | Assert.Equal(0x09, frame.Flags); 26 | Assert.Equal(0x1AB83412u, frame.StreamId); 27 | } 28 | 29 | [Fact] 30 | public void FrameHeadersShouldBeEncodeable() 31 | { 32 | var buf = new byte[FrameHeader.HeaderSize]; 33 | var bufView = new ArraySegment(buf); 34 | var frame = new FrameHeader 35 | { 36 | Length = 0x0340F1, 37 | Type = (FrameType)0xFF, 38 | Flags = 0x09, 39 | StreamId = 0x1AB83412u, 40 | }; 41 | frame.EncodeInto(bufView); 42 | var expected = new byte[] { 0x03, 0x40, 0xF1, 0xFF, 0x09, 0x1A, 0xB8, 0x34, 0x12 }; 43 | Assert.Equal(buf, expected); 44 | frame = new FrameHeader 45 | { 46 | Length = (1 << 24) - 1, 47 | Type = FrameType.Headers, 48 | Flags = 0xFF, 49 | StreamId = 0x7FFFFFFFu, 50 | }; 51 | frame.EncodeInto(bufView); 52 | expected = new byte[] { 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF }; 53 | Assert.Equal(buf, expected); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Http2Tests/TestHeaders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.Extensions.Logging; 5 | 6 | using Http2; 7 | using Http2.Hpack; 8 | 9 | namespace Http2Tests 10 | { 11 | public static class TestHeaders 12 | { 13 | public static readonly HeaderField[] DefaultGetHeaders = new HeaderField[] 14 | { 15 | new HeaderField { Name = ":method", Value = "GET" }, 16 | new HeaderField { Name = ":scheme", Value = "http" }, 17 | new HeaderField { Name = ":path", Value = "/" }, 18 | new HeaderField { Name = "abc", Value = "def" }, 19 | }; 20 | 21 | public static byte[] EncodedDefaultGetHeaders = new byte[] 22 | { 23 | 0x82, // :method GET 24 | 0x86, // :scheme http 25 | 0x84, // :path / 26 | 0x40, 0x03, (byte)'a', (byte)'b', (byte)'c', 27 | 0x03, (byte)'d', (byte)'e', (byte)'f', 28 | }; 29 | 30 | public static byte[] EncodedIndexedDefaultGetHeaders = new byte[] 31 | { 32 | 0x82, // :method GET 33 | 0x86, // :scheme http 34 | 0x84, // :path / 35 | 0xBE, // Indexed header 36 | }; 37 | 38 | public static readonly HeaderField[] DefaultStatusHeaders = new HeaderField[] 39 | { 40 | new HeaderField { Name = ":status", Value = "200" }, 41 | new HeaderField { Name = "xyz", Value = "ghi" }, 42 | }; 43 | 44 | public static byte[] EncodedDefaultStatusHeaders = new byte[] 45 | { 46 | 0x88, 47 | 0x40, 0x03, (byte)'x', (byte)'y', (byte)'z', 48 | 0x03, (byte)'g', (byte)'h', (byte)'i', 49 | }; 50 | 51 | public static readonly HeaderField[] DefaultTrailingHeaders = new HeaderField[] 52 | { 53 | new HeaderField { Name = "trai", Value = "ler" }, 54 | }; 55 | 56 | public static byte[] EncodedDefaultTrailingHeaders = new byte[] 57 | { 58 | 0x40, 0x04, (byte)'t', (byte)'r', (byte)'a', (byte)'i', 59 | 0x03, (byte)'l', (byte)'e', (byte)'r' 60 | }; 61 | } 62 | } -------------------------------------------------------------------------------- /Http2/ByteStreams.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Http2 5 | { 6 | /// 7 | /// The result of a ReadAsync operation 8 | /// 9 | public struct StreamReadResult 10 | { 11 | /// 12 | /// The amount of bytes that were read 13 | /// 14 | public int BytesRead; 15 | 16 | /// 17 | /// Whether the end of the stream was reached 18 | /// In this case no bytes should be read 19 | /// 20 | public bool EndOfStream; 21 | } 22 | 23 | public interface IReadableByteStream 24 | { 25 | /// 26 | /// Reads data from a stream into the given buffer segment. 27 | /// The amound of bytes that will be read is up to the given buffer length 28 | /// The return value signals how many bytes were actually read. 29 | /// 30 | ValueTask ReadAsync(ArraySegment buffer); 31 | } 32 | 33 | public interface IWriteableByteStream 34 | { 35 | /// 36 | /// Writes the buffer to the stream. 37 | /// 38 | Task WriteAsync(ArraySegment buffer); 39 | } 40 | 41 | public interface ICloseableByteStream 42 | { 43 | /// 44 | /// Closes the stream gracefully. 45 | /// This should signal EndOfStream to the receiving side once all prior 46 | /// data has been read. 47 | /// 48 | Task CloseAsync(); 49 | } 50 | 51 | public interface IWriteAndCloseableByteStream 52 | : IWriteableByteStream, ICloseableByteStream 53 | { 54 | } 55 | 56 | /// 57 | /// A marker class that is used to signal the completion of an Async operation. 58 | /// This purely exists since ValueTask<Void> is not valid in C#. 59 | /// 60 | public class DoneHandle 61 | { 62 | private DoneHandle() {} 63 | 64 | /// 65 | /// A static instance of the Handle 66 | /// 67 | public static readonly DoneHandle Instance = new DoneHandle(); 68 | } 69 | } -------------------------------------------------------------------------------- /Http2Tests/FramePrinterTests.cs: -------------------------------------------------------------------------------- 1 | using Http2; 2 | using Xunit; 3 | 4 | namespace Http2Tests 5 | { 6 | public class FramePrinterTests 7 | { 8 | [Fact] 9 | public void ShouldPrintFramesWithASingleFlag() 10 | { 11 | var fh = new FrameHeader 12 | { 13 | Type = FrameType.Continuation, 14 | Flags = (byte)ContinuationFrameFlags.EndOfHeaders, 15 | StreamId = 0x123, 16 | Length = 999, 17 | }; 18 | var res = FramePrinter.PrintFrameHeader(fh); 19 | Assert.Equal("Continuation flags=[EndOfHeaders] streamId=0x00000123 length=999", res); 20 | } 21 | 22 | [Fact] 23 | public void ShouldPrintFramesWithMultipleFlags() 24 | { 25 | var fh = new FrameHeader 26 | { 27 | Type = FrameType.Data, 28 | Flags = (byte)(DataFrameFlags.EndOfStream | DataFrameFlags.Padded), 29 | StreamId = 0x123, 30 | Length = 999, 31 | }; 32 | var res = FramePrinter.PrintFrameHeader(fh); 33 | Assert.Equal("Data flags=[EndOfStream,Padded] streamId=0x00000123 length=999", res); 34 | } 35 | 36 | [Fact] 37 | public void ShouldPrintFramesWithoutFlags() 38 | { 39 | var fh = new FrameHeader 40 | { 41 | Type = FrameType.ResetStream, 42 | Flags = 0, 43 | StreamId = 0x7FFFFFFF, 44 | Length = 999, 45 | }; 46 | var res = FramePrinter.PrintFrameHeader(fh); 47 | Assert.Equal("ResetStream flags=[] streamId=0x7fffffff length=999", res); 48 | } 49 | 50 | [Fact] 51 | public void ShouldPrintFramesWithUnknownFlags() 52 | { 53 | var fh = new FrameHeader 54 | { 55 | Type = FrameType.ResetStream, 56 | Flags = 0x43, 57 | StreamId = 0x123, 58 | Length = 999, 59 | }; 60 | var res = FramePrinter.PrintFrameHeader(fh); 61 | Assert.Equal("ResetStream flags=[0x43] streamId=0x00000123 length=999", res); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Http2Tests/BufferStreams.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Http2; 4 | 5 | namespace Http2Tests 6 | { 7 | public class BufferReadStream : Http2.IReadableByteStream 8 | { 9 | public byte[] Buffer; 10 | public int Written = 0; 11 | public int ReadOffset = 0; 12 | public int NrReads = 0; 13 | private int maxRead; 14 | 15 | public BufferReadStream(int bufferSize, int maxRead) 16 | { 17 | Buffer = new byte[bufferSize]; 18 | this.maxRead = maxRead; 19 | } 20 | 21 | async ValueTask IReadableByteStream.ReadAsync( 22 | ArraySegment buffer) 23 | { 24 | return await Task.Run(() => 25 | { 26 | var available = Written - ReadOffset; 27 | NrReads++; 28 | if (available == 0) 29 | { 30 | return new StreamReadResult 31 | { 32 | BytesRead = 0, 33 | EndOfStream = true, 34 | }; 35 | } 36 | 37 | var toCopy = Math.Min(buffer.Count, available); 38 | toCopy = Math.Min(toCopy, maxRead); 39 | Array.Copy(Buffer, ReadOffset, buffer.Array, buffer.Offset, toCopy); 40 | ReadOffset += toCopy; 41 | 42 | // We can read up the length 43 | return new StreamReadResult 44 | { 45 | BytesRead = toCopy, 46 | EndOfStream = false, 47 | }; 48 | }); 49 | } 50 | } 51 | 52 | public class BufferWriteStream : Http2.IWriteableByteStream 53 | { 54 | public byte[] Buffer; 55 | public int Written = 0; 56 | 57 | public BufferWriteStream(int bufferSize) 58 | { 59 | Buffer = new byte[bufferSize]; 60 | } 61 | 62 | async Task IWriteableByteStream.WriteAsync( 63 | ArraySegment buffer) 64 | { 65 | await Task.Run(() => 66 | { 67 | Array.Copy(buffer.Array, 0, Buffer, Written, buffer.Count); 68 | Written += buffer.Count; 69 | }); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Http2Tests/ConnectionUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | 5 | using Http2; 6 | using Http2.Hpack; 7 | 8 | namespace Http2Tests 9 | { 10 | public static class ConnectionUtils 11 | { 12 | public static async Task BuildEstablishedConnection( 13 | bool isServer, 14 | IBufferedPipe inputStream, 15 | IBufferedPipe outputStream, 16 | ILoggerProvider loggerProvider, 17 | Func streamListener = null, 18 | Settings? localSettings = null, 19 | Settings? remoteSettings = null, 20 | HuffmanStrategy huffmanStrategy = HuffmanStrategy.Never) 21 | { 22 | ILogger logger = null; 23 | if (loggerProvider != null) 24 | { 25 | logger = loggerProvider.CreateLogger("http2Con"); 26 | } 27 | if (streamListener == null) 28 | { 29 | streamListener = (s) => false; 30 | } 31 | 32 | var config = new ConnectionConfigurationBuilder(isServer) 33 | .UseStreamListener(streamListener) 34 | .UseHuffmanStrategy(huffmanStrategy) 35 | .UseSettings(localSettings ?? Settings.Default) 36 | .Build(); 37 | 38 | var conn = new Connection( 39 | config, inputStream, outputStream, 40 | new Connection.Options 41 | { 42 | Logger = logger, 43 | }); 44 | 45 | await PerformHandshakes( 46 | conn, 47 | inputStream, outputStream, 48 | remoteSettings); 49 | 50 | return conn; 51 | } 52 | 53 | public static async Task PerformHandshakes( 54 | this Connection connection, 55 | IBufferedPipe inputStream, 56 | IBufferedPipe outputStream, 57 | Settings? remoteSettings = null) 58 | { 59 | if (connection.IsServer) 60 | { 61 | await ClientPreface.WriteAsync(inputStream); 62 | } 63 | var rsettings = remoteSettings ?? Settings.Default; 64 | await inputStream.WriteSettings(rsettings); 65 | 66 | if (!connection.IsServer) 67 | { 68 | await outputStream.ReadAndDiscardPreface(); 69 | } 70 | await outputStream.ReadAndDiscardSettings(); 71 | await outputStream.AssertSettingsAck(); 72 | await inputStream.WriteSettingsAck(); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /Http2/HeaderUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | 4 | using Http2.Hpack; 5 | 6 | namespace Http2 7 | { 8 | /// 9 | /// Utility methods for working with header lists 10 | /// 11 | internal static class HeaderListExtensionsUtils 12 | { 13 | /// 14 | /// Searches for a content-length field in the header list and extracts 15 | /// it's value. 16 | /// 17 | /// 18 | /// The value of the content-length header field. 19 | /// -1 if the field is absent. 20 | /// Other negative numbers if the value is not a valid. 21 | /// 22 | public static long GetContentLength(this IEnumerable headerFields) 23 | { 24 | foreach (var hf in headerFields) 25 | { 26 | if (hf.Name == "content-length") 27 | { 28 | long result; 29 | if (!long.TryParse( 30 | hf.Value, NumberStyles.Integer, 31 | CultureInfo.InvariantCulture, out result) || result < 0) 32 | { 33 | return -2; 34 | } 35 | return result; 36 | } 37 | } 38 | 39 | return -1; 40 | } 41 | 42 | /// 43 | /// Returns true if the given header fields contain a :status field 44 | /// with a status code in the 100 range. 45 | /// Returns false in all other cases - also if the :status field is 46 | /// malformed. 47 | /// 48 | public static bool IsInformationalHeaders( 49 | this IEnumerable headerFields) 50 | { 51 | foreach (var hf in headerFields) 52 | { 53 | if (hf.Name == ":status") 54 | { 55 | var statusValue = hf.Value; 56 | if (statusValue.Length != 3) return false; 57 | return 58 | statusValue[0] == '1' && 59 | statusValue[1] >= '0' && statusValue[1] <= '9' && 60 | statusValue[2] >= '0' && statusValue[2] <= '9'; 61 | } 62 | else if (hf.Name.Length > 0 && hf.Name[0] != ':') 63 | { 64 | // Don't even look at non pseudoheaders. 65 | // :status must be in front of them. 66 | return false; 67 | } 68 | } 69 | 70 | return false; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /HpackTests/HeaderTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using Xunit; 5 | 6 | using Http2.Hpack; 7 | 8 | namespace HpackTests 9 | { 10 | public class HeaderTableTests 11 | { 12 | class TableEntryComparer : IEqualityComparer 13 | { 14 | public bool Equals(TableEntry x, TableEntry y) 15 | { 16 | return x.Name == y.Name && x.Value == y.Value 17 | && x.NameLen == y.NameLen && x.ValueLen == y.ValueLen; 18 | } 19 | 20 | public int GetHashCode(TableEntry obj) 21 | { 22 | return obj.GetHashCode(); 23 | } 24 | } 25 | 26 | TableEntryComparer ec = new TableEntryComparer(); 27 | 28 | [Fact] 29 | public void ShouldBeAbleToSetDynamicTableSize() 30 | { 31 | var t = new HeaderTable(2001); 32 | Assert.Equal(2001, t.MaxDynamicTableSize); 33 | t.MaxDynamicTableSize = 300; 34 | Assert.Equal(300, t.MaxDynamicTableSize); 35 | } 36 | 37 | [Fact] 38 | public void ShouldReturnItemsFromStaticTable() 39 | { 40 | var t = new HeaderTable(400); 41 | var item = t.GetAt(1); 42 | Assert.Equal( 43 | new TableEntry { Name = ":authority", NameLen = 10, Value = "", ValueLen = 0}, 44 | item, ec); 45 | item = t.GetAt(2); 46 | Assert.Equal( 47 | new TableEntry { Name = ":method", NameLen = 7, Value = "GET", ValueLen = 3}, 48 | item, ec); 49 | item = t.GetAt(61); 50 | Assert.Equal( 51 | new TableEntry { Name = "www-authenticate", NameLen = 16, Value = "", ValueLen = 0}, 52 | item, ec); 53 | } 54 | 55 | [Fact] 56 | public void ShouldReturnItemsFromDynamicTable() 57 | { 58 | var t = new HeaderTable(400); 59 | t.Insert("a", 1, "b", 2); 60 | t.Insert("c", 3, "d", 4); 61 | var item = t.GetAt(62); 62 | Assert.Equal( 63 | new TableEntry { Name = "c", NameLen = 3, Value = "d", ValueLen = 4}, 64 | item, ec); 65 | item = t.GetAt(63); 66 | Assert.Equal( 67 | new TableEntry { Name = "a", NameLen = 1, Value = "b", ValueLen = 2}, 68 | item, ec); 69 | } 70 | 71 | [Fact] 72 | public void ShouldThrowWhenIncorrectlyIndexed() 73 | { 74 | var t = new HeaderTable(400); 75 | Assert.Throws(() => t.GetAt(-1)); 76 | Assert.Throws(() => t.GetAt(0)); 77 | t.GetAt(1); // Valid 78 | t.GetAt(61); // Last valid 79 | Assert.Throws(() => t.GetAt(62)); 80 | 81 | // Put something into the dynamic table and test again 82 | t.Insert("a", 1, "b", 1); 83 | t.Insert("a", 1, "b", 1); 84 | t.GetAt(62); 85 | t.GetAt(63); 86 | Assert.Throws(() => t.GetAt(64)); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Http2/IoStreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Http2 5 | { 6 | /// 7 | /// Extension methods for System.IO.Stream 8 | /// 9 | public static class IoStreamExtensions 10 | { 11 | /// 12 | /// Contains the result of a System.IO.Stream#CreateStreams operation 13 | /// 14 | public struct CreateStreamsResult 15 | { 16 | /// The resulting readable stream 17 | public IReadableByteStream ReadableStream; 18 | /// The resulting writeable stream 19 | public IWriteAndCloseableByteStream WriteableStream; 20 | } 21 | 22 | /// 23 | /// Creates the required stream abstractions on top of a .NET 24 | /// System.IO.Stream. 25 | /// The created stream wrappers will take ownership of the stream. 26 | /// It is not allowed to use the stream directly after this. 27 | /// 28 | public static CreateStreamsResult CreateStreams(this System.IO.Stream stream) 29 | { 30 | if (stream == null) throw new ArgumentNullException(nameof(stream)); 31 | var wrappedStream = new IoStreamWrapper(stream); 32 | return new CreateStreamsResult 33 | { 34 | ReadableStream = wrappedStream, 35 | WriteableStream = wrappedStream, 36 | }; 37 | } 38 | 39 | internal class IoStreamWrapper : IReadableByteStream, IWriteAndCloseableByteStream 40 | { 41 | private readonly System.IO.Stream stream; 42 | 43 | public IoStreamWrapper(System.IO.Stream stream) 44 | { 45 | this.stream = stream; 46 | } 47 | 48 | public ValueTask ReadAsync(ArraySegment buffer) 49 | { 50 | if (buffer.Count == 0) 51 | { 52 | throw new Exception("Reading 0 bytes is not supported"); 53 | } 54 | 55 | var readTask = stream.ReadAsync(buffer.Array, buffer.Offset, buffer.Count); 56 | Task transformedTask = readTask.ContinueWith(tt => 57 | { 58 | if (tt.Exception != null) 59 | { 60 | throw tt.Exception; 61 | } 62 | 63 | var res = tt.Result; 64 | return new StreamReadResult 65 | { 66 | BytesRead = res, 67 | EndOfStream = res == 0, 68 | }; 69 | }); 70 | 71 | return new ValueTask(transformedTask); 72 | } 73 | 74 | public Task WriteAsync(ArraySegment buffer) 75 | { 76 | return stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count); 77 | } 78 | 79 | public Task CloseAsync() 80 | { 81 | stream.Dispose(); 82 | return Task.CompletedTask; 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Http2/ClientPreface.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Http2 7 | { 8 | /// 9 | /// Tools for working with the HTTP/2 client connection preface 10 | /// 11 | public static class ClientPreface 12 | { 13 | /// Contains the connection preface in string format 14 | public const string String = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; 15 | 16 | /// Contains the connection preface as bytes 17 | public static readonly byte[] Bytes = Encoding.ASCII.GetBytes(ClientPreface.String); 18 | 19 | /// The length of the preface 20 | public static int Length 21 | { 22 | get { return Bytes.Length; } 23 | } 24 | 25 | /// 26 | /// Writes the preface to the given stream 27 | /// 28 | public static Task WriteAsync(IWriteableByteStream stream) 29 | { 30 | return stream.WriteAsync(new ArraySegment(Bytes)); 31 | } 32 | 33 | /// 34 | /// Reads the preface from the given stream and compares it to 35 | /// the expected value. 36 | /// Will throw an error if the preface could not be read or if the stream 37 | /// has finished unexpectedly. 38 | /// 39 | public static async ValueTask ReadAsync(IReadableByteStream stream) 40 | { 41 | var buffer = new byte[Length]; 42 | await stream.ReadAll(new ArraySegment(buffer)); 43 | 44 | // Compare with the expected preface 45 | for (var i = 0; i < buffer.Length; i++) 46 | { 47 | if (buffer[i] != Bytes[i]) 48 | { 49 | throw new Exception("Invalid prefix received"); 50 | } 51 | } 52 | 53 | return DoneHandle.Instance; 54 | } 55 | 56 | /// 57 | /// Reads the preface from the given stream and compares it to 58 | /// the expected value. 59 | /// Will throw an error if the preface could not be read or if the stream 60 | /// has finished unexpectedly. 61 | /// 62 | public static async ValueTask ReadAsync( 63 | IReadableByteStream stream, int timeoutMillis) 64 | { 65 | if (timeoutMillis < 0) throw new ArgumentException(nameof(timeoutMillis)); 66 | else if (timeoutMillis == 0) 67 | { 68 | // No timeout 69 | return await ReadAsync(stream); 70 | } 71 | 72 | var cts = new CancellationTokenSource(); 73 | var readTask = ReadAsync(stream).AsTask(); 74 | var timerTask = Task.Delay(timeoutMillis, cts.Token); 75 | 76 | var finishedTask = await Task.WhenAny(readTask, timerTask); 77 | var hasTimeout = ReferenceEquals(timerTask, finishedTask); 78 | // Cancel the timer which might be still running 79 | cts.Cancel(); 80 | cts.Dispose(); 81 | 82 | if (hasTimeout) throw new TimeoutException(); 83 | // No timeout occured 84 | return readTask.Result; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /Http2/IStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | using Http2.Hpack; 6 | 7 | namespace Http2 8 | { 9 | /// 10 | /// Enumerates possible states of a stream 11 | /// 12 | public enum StreamState 13 | { 14 | Idle, 15 | ReservedLocal, 16 | ReservedRemote, 17 | Open, 18 | HalfClosedLocal, 19 | HalfClosedRemote, 20 | Closed, 21 | Reset, // TODO: Should this be an extra state? 22 | } 23 | 24 | /// 25 | /// A HTTP/2 stream 26 | /// 27 | public interface IStream 28 | : IReadableByteStream, IWriteAndCloseableByteStream, IDisposable 29 | { 30 | /// The ID of the stream 31 | uint Id { get; } 32 | /// Returns the current state of the stream 33 | StreamState State { get; } 34 | 35 | /// 36 | /// Cancels the stream. 37 | /// This will cause sending a RESET frame to the remote peer 38 | /// if the stream was not yet fully processed. 39 | /// 40 | void Cancel(); 41 | 42 | /// 43 | /// Reads the list of incoming header fields from the stream. 44 | /// These will include all pseudo header fields. 45 | /// However the validity of the header fields will have been verified. 46 | /// 47 | Task> ReadHeadersAsync(); 48 | 49 | /// 50 | /// Reads the list of incoming trailing header fields from the stream. 51 | /// The validity of the header fields will have been verified. 52 | /// 53 | Task> ReadTrailersAsync(); 54 | 55 | /// 56 | /// Writes a header block for the stream. 57 | /// 58 | /// 59 | /// The list of header fields to write. 60 | /// The headers must contain the required pseudo headers for the 61 | /// type of the stream. The pseudo headers must be at the start 62 | /// of the list. 63 | /// 64 | /// 65 | /// Whether the stream should be closed with the headers. 66 | /// If this is set to true no data frames may be written. 67 | /// 68 | Task WriteHeadersAsync(IEnumerable headers, bool endOfStream); 69 | 70 | /// 71 | /// Writes a block of trailing headers for the stream. 72 | /// The writing side of the stream will automatically be closed 73 | /// through this operation. 74 | /// 75 | /// 76 | /// The list of trailing headers to write. 77 | /// No pseudo headers must be contained in this list. 78 | /// 79 | Task WriteTrailersAsync(IEnumerable headers); 80 | 81 | /// 82 | /// Writes data to the stream and optionally allows to signal the end 83 | /// of the stream. 84 | /// 85 | /// The block of data to write 86 | /// 87 | /// Whether this is the last data block and the stream should be closed 88 | /// after this operation 89 | /// 90 | Task WriteAsync(ArraySegment buffer, bool endOfStream = false); 91 | } 92 | 93 | /// 94 | /// Signals that a stream was reset 95 | /// 96 | public class StreamResetException : Exception 97 | { 98 | } 99 | } -------------------------------------------------------------------------------- /Http2Tests/ConnectionPriorityFrameTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Logging; 3 | 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | using Http2; 8 | 9 | namespace Http2Tests 10 | { 11 | public class ConnectionPriorityFrameTests 12 | { 13 | private readonly ILoggerProvider loggerProvider; 14 | 15 | public ConnectionPriorityFrameTests(ITestOutputHelper outputHelper) 16 | { 17 | this.loggerProvider = new XUnitOutputLoggerProvider(outputHelper); 18 | } 19 | 20 | [Theory] 21 | [InlineData(true, 3, 1, true, 0)] 22 | [InlineData(false, 3, 1, true, 0)] 23 | [InlineData(true, int.MaxValue - 2, int.MaxValue, false, 255)] 24 | [InlineData(false, int.MaxValue - 2, int.MaxValue, false, 255)] 25 | public async Task ConnectionShouldIgnorePriorityData( 26 | bool isServer, uint streamId, 27 | uint streamDependency, bool isExclusive, byte weight) 28 | { 29 | var inPipe = new BufferedPipe(1024); 30 | var outPipe = new BufferedPipe(1024); 31 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 32 | isServer, inPipe, outPipe, loggerProvider); 33 | 34 | var prioData = new PriorityData 35 | { 36 | StreamDependency = streamDependency, 37 | StreamDependencyIsExclusive = isExclusive, 38 | Weight = weight, 39 | }; 40 | await inPipe.WritePriority(streamId, prioData); 41 | 42 | // Send a ping afterwards 43 | // If we get a response the priority frame in between was ignored 44 | var pingData = new byte[8]; 45 | for (var i = 0; i < 8; i++) pingData[i] = (byte)i; 46 | await inPipe.WritePing(pingData, false); 47 | await outPipe.ReadAndDiscardPong(); 48 | } 49 | 50 | [Fact] 51 | public async Task ConnectionShouldGoAwayOnPriorityStreamIdZero() 52 | { 53 | var inPipe = new BufferedPipe(1024); 54 | var outPipe = new BufferedPipe(1024); 55 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 56 | true, inPipe, outPipe, loggerProvider); 57 | 58 | var prioData = new PriorityData 59 | { 60 | StreamDependency = 1, 61 | StreamDependencyIsExclusive = false, 62 | Weight = 0, 63 | }; 64 | await inPipe.WritePriority(0, prioData); 65 | 66 | await outPipe.AssertGoAwayReception(ErrorCode.ProtocolError, 0u); 67 | await outPipe.AssertStreamEnd(); 68 | } 69 | 70 | [Theory] 71 | [InlineData(0)] 72 | [InlineData(PriorityData.Size-1)] 73 | [InlineData(PriorityData.Size+1)] 74 | public async Task ConnectionShouldGoAwayOnInvalidPriorityFrameLength( 75 | int priorityFrameSize) 76 | { 77 | var inPipe = new BufferedPipe(1024); 78 | var outPipe = new BufferedPipe(1024); 79 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 80 | true, inPipe, outPipe, loggerProvider); 81 | 82 | var fh = new FrameHeader 83 | { 84 | Type = FrameType.Priority, 85 | Flags = 0, 86 | Length = priorityFrameSize, 87 | StreamId = 1u, 88 | }; 89 | await inPipe.WriteFrameHeader(fh); 90 | 91 | await outPipe.AssertGoAwayReception(ErrorCode.FrameSizeError, 0u); 92 | await outPipe.AssertStreamEnd(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Hpack/IntDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Http2.Hpack 4 | { 5 | /// 6 | /// Decodes integer values according to the HPACK specification. 7 | /// 8 | public class IntDecoder 9 | { 10 | /// The result of the decode operation 11 | public int Result; 12 | 13 | /// 14 | /// Whether decoding was completed. 15 | /// This is set after a call to decode(). 16 | /// If a complete integer could be decoded from the input buffer 17 | /// the value is true. If a complete integer could not be decoded 18 | /// then more bytes are needed and decodeCont must be called until 19 | /// done is true before reading the result. 20 | /// 21 | public bool Done = true; 22 | 23 | /// Accumulator 24 | private int _acc = 0; 25 | 26 | /// 27 | /// Starts the decoding of an integer number from the input buffer 28 | /// with the given prefix. The input Buffer MUST have at least a 29 | /// single readable byte available at the given offset. If a complete 30 | /// integer could be decoded during this call the result member will 31 | /// be set to the result and the done member will be set to true. 32 | /// Otherwise more data is needed and decodeCont must be called with 33 | /// new Buffer data until done is set to true before reading the result. 34 | /// 35 | public int Decode(int prefixLen, ArraySegment buf) 36 | { 37 | var offset = buf.Offset; 38 | var length = buf.Count; 39 | 40 | var bt = buf.Array[offset]; 41 | offset++; 42 | length--; 43 | var consumed = 1; 44 | 45 | var prefixMask = (1 << (prefixLen)) - 1; 46 | this.Result = bt & prefixMask; 47 | if (prefixMask == this.Result) 48 | { 49 | // Prefix bits are all set to 1 50 | this._acc = 0; 51 | this.Done = false; 52 | consumed += this.DecodeCont(new ArraySegment(buf.Array, offset, length)); 53 | } 54 | else 55 | { 56 | // Variable is in the prefix 57 | this.Done = true; 58 | } 59 | 60 | return consumed; 61 | } 62 | 63 | /// 64 | /// Continue to decode an integer using the new input buffer data. 65 | /// 66 | public int DecodeCont(ArraySegment buf) 67 | { 68 | var offset = buf.Offset; 69 | var length = buf.Count; 70 | 71 | // Try to decode as long as we have bytes available 72 | while (length > 0) 73 | { 74 | var bt = buf.Array[offset]; 75 | offset++; 76 | length--; 77 | 78 | // Calculate new result 79 | // Thereby check for overflows 80 | var add = (bt & 127) * (1u << _acc); 81 | var n = add + this.Result; 82 | if (n > Int32.MaxValue) 83 | { 84 | throw new Exception("invalid integer"); 85 | } 86 | 87 | this.Result = (int)n; 88 | this._acc += 7; 89 | 90 | if ((bt & 0x80) == 0) 91 | { 92 | // First bit is not set - we're done 93 | this.Done = true; 94 | return offset - buf.Offset; 95 | } 96 | } 97 | 98 | return offset - buf.Offset; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /HpackTests/Huffman.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | using Xunit; 5 | 6 | using Http2.Hpack; 7 | 8 | namespace HpackTests 9 | { 10 | public class HuffmanDecodeTests 11 | { 12 | private ArrayPool bufPool = ArrayPool.Shared; 13 | 14 | [Fact] 15 | public void ShouldDecodeValidHuffmanCodes() 16 | { 17 | var buffer = new Buffer(); 18 | buffer.WriteByte(0xff); 19 | buffer.WriteByte(0xc7); 20 | var decoded = Huffman.Decode(buffer.View, bufPool); 21 | Assert.Equal(decoded.Length, 1); 22 | Assert.Equal(decoded[0], 0); 23 | 24 | buffer = new Buffer(); 25 | buffer.WriteByte(0xf8); // '&' 26 | decoded = Huffman.Decode(buffer.View, bufPool); 27 | Assert.Equal(decoded.Length, 1); 28 | Assert.Equal(decoded[0], '&'); 29 | 30 | buffer = new Buffer(); 31 | buffer.WriteByte(0x59); // 0101 1001 32 | buffer.WriteByte(0x7f); // 0111 1111 33 | buffer.WriteByte(0xff); // 1111 1111 34 | buffer.WriteByte(0xe1); // 1110 0001 35 | decoded = Huffman.Decode(buffer.View, bufPool); 36 | Assert.Equal(decoded.Length, 3); 37 | Assert.Equal(decoded[0], '-'); 38 | Assert.Equal(decoded[1], '.'); 39 | Assert.Equal(decoded[2], '\\'); 40 | 41 | buffer = new Buffer(); 42 | buffer.WriteByte(0x86); // AB = 100001 1011101 = 1000 0110 1110 1 43 | buffer.WriteByte(0xEF); 44 | decoded = Huffman.Decode(buffer.View, bufPool); 45 | Assert.Equal(decoded.Length, 2); 46 | Assert.Equal(decoded[0], 'A'); 47 | Assert.Equal(decoded[1], 'B'); 48 | } 49 | 50 | [Theory] 51 | [InlineData(true)] 52 | [InlineData(false)] 53 | public void ShouldThrowErrorIfEOSSymbolIsEncountered(bool fillLastByte) 54 | { 55 | var buffer = new Buffer(); 56 | buffer.WriteByte(0xf8); // '&' 57 | buffer.WriteByte(0xff); 58 | buffer.WriteByte(0xff); 59 | buffer.WriteByte(0xff); 60 | byte lastByte = fillLastByte ? (byte)0xff : (byte)0xfc; // Both contain EOS 61 | buffer.WriteByte(lastByte); 62 | var ex = Assert.Throws(() => { 63 | Huffman.Decode(buffer.View, bufPool); 64 | }); 65 | Assert.Equal(ex.Message, "Encountered EOS in huffman code"); 66 | } 67 | 68 | [Fact] 69 | public void ShouldThrowErrorIfPaddingIsZero() 70 | { 71 | var buffer = new Buffer(); 72 | buffer.WriteByte(0x86); // AB = 100001 1011101 = 1000 0110 1110 1 73 | buffer.WriteByte(0xE8); 74 | var ex = Assert.Throws(() => { 75 | Huffman.Decode(buffer.View, bufPool); 76 | }); 77 | Assert.Equal("Invalid padding", ex.Message); 78 | } 79 | 80 | [Fact] 81 | public void ShouldThrowErrorIfPaddingIsLongerThanNecessary() 82 | { 83 | var buffer = new Buffer(); 84 | buffer.WriteByte(0x86); // AB = 100001 1011101 = 1000 0110 1110 1 85 | buffer.WriteByte(0xEF); 86 | buffer.WriteByte(0xFF); // Extra padding 87 | var ex = Assert.Throws(() => { 88 | Huffman.Decode(buffer.View, bufPool); 89 | }); 90 | Assert.Equal("Padding exceeds 7 bits", ex.Message); 91 | 92 | buffer = new Buffer(); 93 | buffer.WriteByte(0xFA); // ',' = 0xFA 94 | buffer.WriteByte(0xFF); // Padding 95 | ex = Assert.Throws(() => { 96 | Huffman.Decode(buffer.View, bufPool); 97 | }); 98 | Assert.Equal("Padding exceeds 7 bits", ex.Message); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Http2Tests/ClientPrefaceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using System.Text; 5 | 6 | using Xunit; 7 | 8 | using Http2; 9 | 10 | namespace Http2Tests 11 | { 12 | public class ClientPrefaceTests 13 | { 14 | [Fact] 15 | public void PrefixShouldBy24BytesLong() 16 | { 17 | Assert.Equal(24, ClientPreface.Length); 18 | } 19 | 20 | [Fact] 21 | public void PrefixShouldBeCorrectASCIIEncoded() 22 | { 23 | Assert.Equal( 24 | new byte[] { 25 | 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, 0x54, 0x50, 26 | 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a, 0x53, 0x4d, 27 | 0x0d, 0x0a, 0x0d, 0x0a }, 28 | ClientPreface.Bytes); 29 | } 30 | 31 | [Fact] 32 | public async Task ShouldWriteThePrefaceToStream() 33 | { 34 | var buffer = new BufferWriteStream(50); 35 | await ClientPreface.WriteAsync(buffer); 36 | Assert.Equal(ClientPreface.Length, buffer.Written); 37 | 38 | var pf = Encoding.ASCII.GetString(buffer.Buffer, 0, ClientPreface.Length); 39 | Assert.Equal(ClientPreface.String, pf); 40 | } 41 | 42 | [Fact] 43 | public async Task ShouldReadThePrefaceFromStream() 44 | { 45 | var buffer = new BufferReadStream(50, 50); 46 | Array.Copy(ClientPreface.Bytes, buffer.Buffer, ClientPreface.Length); 47 | buffer.Written = ClientPreface.Length; 48 | await ClientPreface.ReadAsync(buffer); 49 | } 50 | 51 | [Fact] 52 | public async Task ShouldErrorIfStreamEnds() 53 | { 54 | var buffer = new BufferReadStream(50, 50); 55 | Array.Copy(ClientPreface.Bytes, buffer.Buffer, ClientPreface.Length); 56 | buffer.Written = ClientPreface.Length - 1; // Miss one byte 57 | await Assert.ThrowsAsync( 58 | () => ClientPreface.ReadAsync(buffer).AsTask()); 59 | } 60 | 61 | [Fact] 62 | public async Task ShouldErrorIfStreamDoesNotContainPreface() 63 | { 64 | var buffer = new BufferReadStream(50, 50); 65 | Array.Copy(ClientPreface.Bytes, buffer.Buffer, ClientPreface.Length); 66 | buffer.Buffer[22] = (byte)'l'; 67 | buffer.Written = ClientPreface.Length; 68 | var ex = await Assert.ThrowsAsync( 69 | () => ClientPreface.ReadAsync(buffer).AsTask()); 70 | Assert.Equal("Invalid prefix received", ex.Message); 71 | } 72 | 73 | [Fact] 74 | public async Task ShouldErrorIfPrefaceWasNotReceivedUntilTimeout() 75 | { 76 | var timeout = 50; 77 | var pipe = new BufferedPipe(50); 78 | await Assert.ThrowsAsync( 79 | () => ClientPreface.ReadAsync(pipe, timeout).AsTask()); 80 | } 81 | 82 | [Fact] 83 | public async Task ShouldNotErrorIfPrefaceWasReceivedWithinTimeout() 84 | { 85 | var timeout = 200; 86 | var pipe = new BufferedPipe(50); 87 | var _ = Task.Run(async () => 88 | { 89 | await pipe.WriteAsync(new ArraySegment(ClientPreface.Bytes)); 90 | }); 91 | await ClientPreface.ReadAsync(pipe, timeout); 92 | } 93 | 94 | [Fact] 95 | public async Task ShouldErrorIfStreamWasClosedWithinTimeout() 96 | { 97 | var timeout = 200; 98 | var pipe = new BufferedPipe(50); 99 | var _ = Task.Run(async () => 100 | { 101 | await Task.Delay(10); 102 | await pipe.CloseAsync(); 103 | }); 104 | var ex = await Assert.ThrowsAsync( 105 | () => ClientPreface.ReadAsync(pipe, timeout).AsTask()); 106 | Assert.IsType(ex.InnerException); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /HpackTests/IntEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using Http2.Hpack; 4 | 5 | namespace HpackTests 6 | { 7 | public class IntEncoderTests 8 | { 9 | [Fact] 10 | public void ShouldEncodeValuesWhichFitIntoThePrefix() 11 | { 12 | int val = 30; // Fits into 5bit prefix 13 | var buf = IntEncoder.Encode(val, 0x80, 5); 14 | Assert.Equal(buf.Length, 1); 15 | Assert.Equal(buf[0], 0x80 | 0x1E); 16 | 17 | val = 1; // Fits into 2 bit prefix 18 | buf = IntEncoder.Encode(val, 0xFC, 2); 19 | Assert.Equal(buf.Length, 1); 20 | Assert.Equal(buf[0], 0xFC | 0x01); 21 | 22 | val = 128; // Fits into 8 bit prefix 23 | buf = IntEncoder.Encode(val, 0x00, 8); 24 | Assert.Equal(buf.Length, 1); 25 | Assert.Equal(buf[0], 0x80); 26 | 27 | val = 254; // Fits into 8 bit prefix 28 | buf = IntEncoder.Encode(val, 0x00, 8); 29 | Assert.Equal(buf.Length, 1); 30 | Assert.Equal(buf[0], 254); 31 | } 32 | 33 | [Fact] 34 | public void ShouldEncodeValuesIntoPrefixPlusExtraBytes() 35 | { 36 | int val = 30; // Fits not into 4bit prefix 37 | var buf = IntEncoder.Encode(val, 0xA0, 4); 38 | Assert.Equal(buf.Length, 2); 39 | Assert.Equal(buf[0], 0xA0 | 0x0F); 40 | Assert.Equal(buf[1], 15); // 30 - 15 = 15 41 | 42 | val = 1; // Fits not into 1bit prefix 43 | buf = IntEncoder.Encode(val, 0xFE, 1); 44 | Assert.Equal(buf.Length, 2); 45 | Assert.Equal(buf[0], 0xFE | 0x01); 46 | Assert.Equal(buf[1], 0); 47 | 48 | val = 127; // Fits not into 1bit prefix 49 | buf = IntEncoder.Encode(val, 0x80, 7); 50 | Assert.Equal(buf.Length, 2); 51 | Assert.Equal(buf[0], 0x80 | 0xFF); 52 | Assert.Equal(buf[1], 0); 53 | 54 | val = 128; // Fits not into 1bit prefix 55 | buf = IntEncoder.Encode(val, 0x00, 7); 56 | Assert.Equal(buf.Length, 2); 57 | Assert.Equal(buf[0], 0x00 | 0x7F); 58 | Assert.Equal(buf[1], 1); 59 | 60 | val = 255; // Fits not into 8 bit prefix 61 | buf = IntEncoder.Encode(val, 0x00, 8); 62 | Assert.Equal(buf.Length, 2); 63 | Assert.Equal(buf[0], 0xFF); 64 | Assert.Equal(buf[1], 0); 65 | 66 | val = 256; // Fits not into 8 bit prefix 67 | buf = IntEncoder.Encode(val, 0x00, 8); 68 | Assert.Equal(buf.Length, 2); 69 | Assert.Equal(buf[0], 0xFF); 70 | Assert.Equal(buf[1], 1); 71 | 72 | val = 1337; // 3byte example from the spec 73 | buf = IntEncoder.Encode(val, 0xC0, 5); 74 | Assert.Equal(buf.Length, 3); 75 | Assert.Equal(buf[0], 0xC0 | 0x1F); 76 | Assert.Equal(buf[1], 0x9A); 77 | Assert.Equal(buf[2], 0x0A); 78 | 79 | // 4 byte example 80 | val = 27 * 128 * 128 + 31 * 128 + 1; 81 | buf = IntEncoder.Encode(val, 0, 1); 82 | Assert.Equal(buf.Length, 4); 83 | Assert.Equal(buf[0], 1); 84 | Assert.Equal(buf[1], 0x80); 85 | Assert.Equal(buf[2], 0x9F); 86 | Assert.Equal(buf[3], 27); 87 | } 88 | 89 | [Fact] 90 | public void ShouldEncodeTheMaximumAllowedValue() 91 | { 92 | int val = Int32.MaxValue; // Fits not into 4bit prefix 93 | var buf = IntEncoder.Encode(val, 0xA0, 2); 94 | Assert.Equal(buf.Length, 6); 95 | Assert.Equal(buf[0], 0xA3); // Remaining: 2147483644 96 | Assert.Equal(buf[1], 252); // Remaining: 16777215 97 | Assert.Equal(buf[2], 255); // Remaining: 131071 98 | Assert.Equal(buf[3], 255); // Remaining: 1023 99 | Assert.Equal(buf[4], 255); // Remaining: 7 100 | Assert.Equal(buf[5], 7); 101 | 102 | // TODO: Probably test this with other prefixes 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Hpack/HeaderTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Http2.Hpack 4 | { 5 | /// 6 | /// The combination of the static and a dynamic header table 7 | /// 8 | public class HeaderTable 9 | { 10 | DynamicTable dynamic; 11 | 12 | public HeaderTable(int dynamicTableSize) 13 | { 14 | this.dynamic = new DynamicTable(dynamicTableSize); 15 | } 16 | 17 | public int MaxDynamicTableSize 18 | { 19 | get { return this.dynamic.MaxTableSize; } 20 | set { this.dynamic.MaxTableSize = value; } 21 | } 22 | 23 | /// Gets the occupied size in bytes for the dynamic table 24 | public int UsedDynamicTableSize => this.dynamic.UsedSize; 25 | 26 | /// Gets the current length of the dynamic table 27 | public int DynamicTableLength => this.dynamic.Length; 28 | 29 | /// 30 | /// Inserts a new element into the header table 31 | /// 32 | public bool Insert(string name, int nameBytes, string value, int valueBytes) 33 | { 34 | return dynamic.Insert(name, nameBytes, value, valueBytes); 35 | } 36 | 37 | public TableEntry GetAt(int index) 38 | { 39 | // 0 is not a valid index 40 | if (index < 1) throw new IndexOutOfRangeException(); 41 | 42 | // Relate index to start of static table 43 | // and look if element is in there 44 | index--; 45 | if (index < StaticTable.Entries.Length) 46 | { 47 | // Index is in the static table 48 | return StaticTable.Entries[index]; 49 | } 50 | 51 | // Relate index to start of dynamic table 52 | // and look if element is in there 53 | index -= StaticTable.Entries.Length; 54 | if (index < this.dynamic.Length) 55 | { 56 | return this.dynamic.GetAt(index); 57 | } 58 | 59 | // Element is not in static or dynamic table 60 | throw new IndexOutOfRangeException(); 61 | } 62 | 63 | /// 64 | /// Returns the index of the best matching element in the header table. 65 | /// If no index was found the return value is -1. 66 | /// If an index was found and the name as well as the value match 67 | /// isFullMatch will be set to true. 68 | /// 69 | public int GetBestMatchingIndex(HeaderField field, out bool isFullMatch) 70 | { 71 | var bestMatch = -1; 72 | isFullMatch = false; 73 | 74 | var i = 1; 75 | foreach (var entry in StaticTable.Entries) 76 | { 77 | if (entry.Name == field.Name) 78 | { 79 | if (bestMatch == -1) 80 | { 81 | // We used the lowest matching field index, which makes 82 | // search for the receiver the most efficient and provides 83 | // the highest chance to use the Static Table. 84 | bestMatch = i; 85 | } 86 | 87 | if (entry.Value == field.Value) 88 | { 89 | // It's a perfect match! 90 | isFullMatch = true; 91 | return i; 92 | } 93 | } 94 | i++; 95 | } 96 | 97 | // If we don't have a full match search on in the dynamic table 98 | bool dynamicHasFullMatch; 99 | var di = this.dynamic.GetBestMatchingIndex(field, out dynamicHasFullMatch); 100 | if (dynamicHasFullMatch) 101 | { 102 | isFullMatch = true; 103 | return di + 1 + StaticTable.Length; 104 | } 105 | // If the dynamic table has a match at all use it's index and normalize it 106 | if (di != -1 && bestMatch == -1) bestMatch = di + 1 + StaticTable.Length; 107 | 108 | return bestMatch; 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /Http2Tests/ConnectionResetStreamTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | using Http2; 9 | using Http2.Hpack; 10 | 11 | namespace Http2Tests 12 | { 13 | public class ConnectionResetStreamTests 14 | { 15 | private readonly ILoggerProvider loggerProvider; 16 | 17 | public ConnectionResetStreamTests(ITestOutputHelper outputHelper) 18 | { 19 | this.loggerProvider = new XUnitOutputLoggerProvider(outputHelper); 20 | } 21 | 22 | [Theory] 23 | [InlineData(true)] 24 | [InlineData(false)] 25 | public async Task ConnectionShouldGoAwayOnInvalidResetStreamId( 26 | bool isServer) 27 | { 28 | var inPipe = new BufferedPipe(1024); 29 | var outPipe = new BufferedPipe(1024); 30 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 31 | isServer, inPipe, outPipe, loggerProvider); 32 | 33 | await inPipe.WriteResetStream(0, ErrorCode.Cancel); 34 | await outPipe.AssertGoAwayReception(ErrorCode.ProtocolError, 0); 35 | await outPipe.AssertStreamEnd(); 36 | } 37 | 38 | [Theory] 39 | [InlineData(false, 0)] 40 | [InlineData(false, ResetFrameData.Size-1)] 41 | [InlineData(false, ResetFrameData.Size+1)] 42 | [InlineData(true, 0)] 43 | [InlineData(true, ResetFrameData.Size-1)] 44 | [InlineData(true, ResetFrameData.Size+1)] 45 | public async Task ConnectionShouldGoAwayOnInvalidResetStreamFrameLength( 46 | bool isServer, int resetFrameLength) 47 | { 48 | var inPipe = new BufferedPipe(1024); 49 | var outPipe = new BufferedPipe(1024); 50 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 51 | isServer, inPipe, outPipe, loggerProvider); 52 | 53 | var rstStreamHeader = new FrameHeader 54 | { 55 | Type = FrameType.ResetStream, 56 | Flags = 0, 57 | Length = resetFrameLength, 58 | StreamId = 1, 59 | }; 60 | await inPipe.WriteFrameHeader(rstStreamHeader); 61 | await outPipe.AssertGoAwayReception(ErrorCode.FrameSizeError, 0); 62 | await outPipe.AssertStreamEnd(); 63 | } 64 | 65 | [Fact] 66 | public async Task ConnectionShouldIgnoreResetsforUnknownStreams() 67 | { 68 | var inPipe = new BufferedPipe(1024); 69 | var outPipe = new BufferedPipe(1024); 70 | 71 | Func listener = (s) => true; 72 | var conn = await ConnectionUtils.BuildEstablishedConnection( 73 | true, inPipe, outPipe, loggerProvider, listener); 74 | var hEncoder = new Encoder(); 75 | 76 | var streamId = 7u; 77 | await inPipe.WriteHeaders( 78 | hEncoder, streamId, false, TestHeaders.DefaultGetHeaders); 79 | 80 | await inPipe.WriteResetStream(streamId - 2, ErrorCode.RefusedStream); 81 | await inPipe.WriteResetStream(streamId - 4, ErrorCode.Cancel); 82 | 83 | // Send a ping afterwards 84 | // If we get a response the reset frame in between was ignored 85 | await inPipe.WritePing(new byte[8], false); 86 | await outPipe.ReadAndDiscardPong(); 87 | } 88 | 89 | [Theory] 90 | [InlineData(true, 1u)] 91 | [InlineData(false, 1u)] 92 | [InlineData(true, 2u)] 93 | [InlineData(false, 2u)] 94 | public async Task ResetsOnIdleStreamsShouldBeTreatedAsConnectionError( 95 | bool isServer, uint streamId) 96 | { 97 | var inPipe = new BufferedPipe(1024); 98 | var outPipe = new BufferedPipe(1024); 99 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 100 | isServer, inPipe, outPipe, loggerProvider); 101 | 102 | await inPipe.WriteResetStream(streamId, ErrorCode.RefusedStream); 103 | await outPipe.AssertGoAwayReception(ErrorCode.ProtocolError, 0u); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /Examples/ExampleServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Logging.Console; 9 | using Http2; 10 | using Http2.Hpack; 11 | 12 | class Program 13 | { 14 | static void Main(string[] args) 15 | { 16 | var logProvider = new ConsoleLoggerProvider((s, level) => true, true); 17 | // Create a TCP socket acceptor 18 | var listener = new TcpListener(IPAddress.Any, 8888); 19 | listener.Start(); 20 | Task.Run(() => AcceptTask(listener, logProvider)).Wait(); 21 | } 22 | 23 | static bool AcceptIncomingStream(IStream stream) 24 | { 25 | Task.Run(() => HandleIncomingStream(stream)); 26 | return true; 27 | } 28 | 29 | static byte[] responseBody = Encoding.ASCII.GetBytes( 30 | "Hello WorldContent"); 31 | 32 | static async void HandleIncomingStream(IStream stream) 33 | { 34 | try 35 | { 36 | // Read the headers 37 | var headers = await stream.ReadHeadersAsync(); 38 | var method = headers.First(h => h.Name == ":method").Value; 39 | var path = headers.First(h => h.Name == ":path").Value; 40 | // Print the request method and path 41 | Console.WriteLine("Method: {0}, Path: {1}", method, path); 42 | 43 | // Read the request body and write it to console 44 | var buf = new byte[2048]; 45 | while (true) 46 | { 47 | var readResult = await stream.ReadAsync(new ArraySegment(buf)); 48 | if (readResult.EndOfStream) break; 49 | // Print the received bytes 50 | Console.WriteLine(Encoding.ASCII.GetString(buf, 0, readResult.BytesRead)); 51 | } 52 | 53 | // Send a response which consists of headers and a payload 54 | var responseHeaders = new HeaderField[] { 55 | new HeaderField { Name = ":status", Value = "200" }, 56 | new HeaderField { Name = "content-type", Value = "text/html" }, 57 | }; 58 | await stream.WriteHeadersAsync(responseHeaders, false); 59 | await stream.WriteAsync(new ArraySegment( 60 | responseBody), true); 61 | 62 | // Request is fully handled here 63 | } 64 | catch (Exception e) 65 | { 66 | Console.WriteLine("Error during handling request: {0}", e.Message); 67 | stream.Cancel(); 68 | } 69 | } 70 | 71 | static async Task AcceptTask(TcpListener listener, ILoggerProvider logProvider) 72 | { 73 | var connectionId = 0; 74 | 75 | var settings = Settings.Default; 76 | settings.MaxConcurrentStreams = 50; 77 | 78 | var config = 79 | new ConnectionConfigurationBuilder(true) 80 | .UseStreamListener(AcceptIncomingStream) 81 | .UseSettings(settings) 82 | .UseHuffmanStrategy(HuffmanStrategy.IfSmaller) 83 | .Build(); 84 | 85 | while (true) 86 | { 87 | // Accept TCP sockets 88 | var clientSocket = await listener.AcceptSocketAsync(); 89 | clientSocket.NoDelay = true; 90 | // Create HTTP/2 stream abstraction on top of the socket 91 | var wrappedStreams = clientSocket.CreateStreams(); 92 | // Alternatively on top of a System.IO.Stream 93 | //var netStream = new NetworkStream(clientSocket, true); 94 | //var wrappedStreams = netStream.CreateStreams(); 95 | 96 | // Build a HTTP connection on top of the stream abstraction 97 | var http2Con = new Connection( 98 | config, wrappedStreams.ReadableStream, wrappedStreams.WriteableStream, 99 | options: new Connection.Options 100 | { 101 | Logger = logProvider.CreateLogger("HTTP2Conn" + connectionId), 102 | }); 103 | 104 | // Close the connection if we get a GoAway from the client 105 | var remoteGoAwayTask = http2Con.RemoteGoAwayReason; 106 | var closeWhenRemoteGoAway = Task.Run(async () => 107 | { 108 | await remoteGoAwayTask; 109 | await http2Con.GoAwayAsync(ErrorCode.NoError, true); 110 | }); 111 | 112 | connectionId++; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Hpack/IntEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Http2.Hpack 4 | { 5 | /// 6 | /// Encodes integer values according to the HPACK specification. 7 | /// 8 | public static class IntEncoder 9 | { 10 | /// 11 | /// Returns the number of bytes that is required for encoding 12 | /// the given value. 13 | /// 14 | /// The value to encode 15 | /// The value that is stored in the same byte as the prefix 16 | /// The number of bits that shall be used for the prefix 17 | /// The number of required bytes 18 | public static int RequiredBytes(int value, byte beforePrefix, int prefixBits) 19 | { 20 | if (value < 0) throw new ArgumentOutOfRangeException(nameof(value)); 21 | 22 | var offset = 0; 23 | 24 | // Calculate the maximum value that fits into the prefix 25 | int maxPrefixVal = ((1 << prefixBits) - 1); // equals 2^N - 1 26 | if (value < maxPrefixVal) { 27 | // Value fits into the prefix 28 | offset++; 29 | } else { 30 | offset++; 31 | value -= maxPrefixVal; 32 | while (value >= 128) 33 | { 34 | offset++; 35 | value = value / 128; // Shift is not valid above 32bit 36 | } 37 | offset++; 38 | } 39 | 40 | return offset; 41 | } 42 | 43 | /// 44 | /// Encodes the given number into the target buffer 45 | /// 46 | /// The target buffer for encoding the value 47 | /// The value to encode 48 | /// The value that is stored in the same byte as the prefix 49 | /// The number of bits that shall be used for the prefix 50 | /// 51 | /// The number of bytes that were required to encode the value. 52 | /// -1 if the value did not fit into the buffer. 53 | /// 54 | public static int EncodeInto( 55 | ArraySegment buf, int value, byte beforePrefix, int prefixBits) 56 | { 57 | if (value < 0) throw new ArgumentOutOfRangeException(nameof(value)); 58 | 59 | var offset = buf.Offset; 60 | int free = buf.Count; 61 | if (free < 1) return -1; 62 | 63 | // Calculate the maximum value that fits into the prefix 64 | int maxPrefixVal = ((1 << prefixBits) - 1); // equals 2^N - 1 65 | if (value < maxPrefixVal) { 66 | // Value fits into the prefix 67 | buf.Array[offset] = (byte)((beforePrefix | value) & 0xFF); 68 | offset++; 69 | } else { 70 | // Value does not fit into prefix 71 | // Save the max prefix value 72 | buf.Array[offset] = (byte)((beforePrefix | maxPrefixVal) & 0xFF); 73 | offset++; 74 | free--; 75 | if (free < 1) return -1; 76 | value -= maxPrefixVal; 77 | while (value >= 128) 78 | { 79 | var part = (value % 128 + 128); 80 | buf.Array[offset] = (byte)(part & 0xFF); 81 | offset++; 82 | free--; 83 | if (free < 1) return -1; 84 | value = value / 128; // Shift is not valid above 32bit 85 | } 86 | buf.Array[offset] = (byte)(value & 0xFF); 87 | offset++; 88 | } 89 | 90 | return offset - buf.Offset; 91 | } 92 | 93 | /// 94 | /// Encodes the given number. 95 | /// 96 | /// The value to encode 97 | /// The value that is stored in the same byte as the prefix 98 | /// The number of bits that shall be used for the prefix 99 | /// The encoded number 100 | public static byte[] Encode(int value, byte beforePrefix, int prefixBits) 101 | { 102 | var bytes = new byte[RequiredBytes(value, beforePrefix, prefixBits)]; 103 | EncodeInto(new ArraySegment(bytes), value, beforePrefix, prefixBits); 104 | return bytes; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Examples/Shared/UpgradeReadStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Logging.Console; 10 | using Http2; 11 | using Http2.Hpack; 12 | 13 | /// 14 | /// Wrapper around the readable stream, which allows to read HTTP/1 15 | /// request headers. 16 | /// 17 | class UpgradeReadStream : IReadableByteStream 18 | { 19 | IReadableByteStream stream; 20 | byte[] httpBuffer = new byte[MaxHeaderLength]; 21 | int httpBufferOffset = 0; 22 | int httpHeaderLength = 0; 23 | 24 | ArraySegment remains; 25 | 26 | const int MaxHeaderLength = 1024; 27 | 28 | public int HttpHeaderLength => httpHeaderLength; 29 | 30 | public ArraySegment HeaderBytes => 31 | new ArraySegment(httpBuffer, 0, HttpHeaderLength); 32 | 33 | public UpgradeReadStream(IReadableByteStream stream) 34 | { 35 | this.stream = stream; 36 | } 37 | 38 | /// 39 | /// Waits until a whole HTTP/1 header, terminated by \r\n\r\n was received. 40 | /// This may only be called once at the beginning of the stream. 41 | /// If the header was found it can be accessed with HeaderBytes. 42 | /// Then it must be either consumed or marked as unread. 43 | /// 44 | public async Task WaitForHttpHeader() 45 | { 46 | while (true) 47 | { 48 | var res = await stream.ReadAsync( 49 | new ArraySegment(httpBuffer, httpBufferOffset, httpBuffer.Length - httpBufferOffset)); 50 | 51 | if (res.EndOfStream) 52 | throw new System.IO.EndOfStreamException(); 53 | httpBufferOffset += res.BytesRead; 54 | 55 | // Check for end of headers in the received data 56 | var str = Encoding.ASCII.GetString(httpBuffer, 0, httpBufferOffset); 57 | var endOfHeaderIndex = str.IndexOf("\r\n\r\n"); 58 | if (endOfHeaderIndex == -1) 59 | { 60 | // Header end not yet found 61 | if (httpBufferOffset == httpBuffer.Length) 62 | { 63 | httpBuffer = null; 64 | throw new Exception("No HTTP header received"); 65 | } 66 | // else read more bytes by looping around 67 | } 68 | else 69 | { 70 | httpHeaderLength = endOfHeaderIndex + 4; 71 | return; 72 | } 73 | } 74 | } 75 | 76 | /// 77 | /// Marks the HTTP reader as unread, which means following 78 | /// ReadAsync calls will reread the header. 79 | /// 80 | public void UnreadHttpHeader() 81 | { 82 | remains = new ArraySegment( 83 | httpBuffer, 0, httpBufferOffset); 84 | } 85 | 86 | /// Removes the received HTTP header from the input buffer 87 | public void ConsumeHttpHeader() 88 | { 89 | if (httpHeaderLength != httpBufferOffset) 90 | { 91 | // Not everything was consumed 92 | remains = new ArraySegment( 93 | httpBuffer, httpHeaderLength, httpBufferOffset - httpHeaderLength); 94 | } 95 | else 96 | { 97 | remains = new ArraySegment(); 98 | httpBuffer = null; 99 | } 100 | } 101 | 102 | public ValueTask ReadAsync(ArraySegment buffer) 103 | { 104 | if (remains.Count != 0) 105 | { 106 | // Return leftover bytes from upgrade request 107 | var toCopy = Math.Min(remains.Count, buffer.Count); 108 | Array.Copy( 109 | remains.Array, remains.Offset, 110 | buffer.Array, buffer.Offset, 111 | toCopy); 112 | var newOffset = remains.Offset + toCopy; 113 | var newCount = remains.Count - toCopy; 114 | if (newCount != 0) 115 | { 116 | remains = new ArraySegment(remains.Array, newOffset, newCount); 117 | } 118 | else 119 | { 120 | remains = new ArraySegment(); 121 | httpBuffer = null; 122 | } 123 | 124 | return new ValueTask( 125 | new StreamReadResult() 126 | { 127 | BytesRead = toCopy, 128 | EndOfStream = false, 129 | }); 130 | } 131 | 132 | return stream.ReadAsync(buffer); 133 | } 134 | } -------------------------------------------------------------------------------- /Examples/Shared/Http1Types.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Logging.Console; 10 | using Http2; 11 | using Http2.Hpack; 12 | 13 | /// 14 | /// A primitive HTTP/1 request header parser 15 | /// 16 | class Http1Request 17 | { 18 | public string Method; 19 | public string Path; 20 | public string Protocol; 21 | 22 | private static Exception InvalidRequestHeaderException = 23 | new Exception("Invalid request header"); 24 | 25 | public Dictionary Headers = new Dictionary(); 26 | 27 | private static System.Text.RegularExpressions.Regex requestLineRegExp = 28 | new System.Text.RegularExpressions.Regex( 29 | @"([^\s]+) ([^\s]+) ([^\s]+)"); 30 | 31 | public static Http1Request ParseFrom(string requestHeader) 32 | { 33 | var lines = requestHeader.Split(new string[]{"\r\n"}, StringSplitOptions.None); 34 | if (lines.Length < 1) throw InvalidRequestHeaderException; 35 | 36 | // Parse request line in form GET /page HTTP/1.1 37 | // This is a super simple and bad parser 38 | 39 | var match = requestLineRegExp.Match(lines[0]); 40 | if (!match.Success) throw InvalidRequestHeaderException; 41 | 42 | var method = match.Groups[1].Value; 43 | var path = match.Groups[2].Value; 44 | var proto = match.Groups[3].Value; 45 | if (string.IsNullOrEmpty(method) || 46 | string.IsNullOrEmpty(path) || 47 | string.IsNullOrEmpty(proto)) 48 | throw InvalidRequestHeaderException; 49 | 50 | var headers = new Dictionary(); 51 | for (var i = 1; i < lines.Length; i++) 52 | { 53 | var line = lines[i]; 54 | var colonIdx = line.IndexOf(':'); 55 | if (colonIdx == -1) throw InvalidRequestHeaderException; 56 | var name = line.Substring(0, colonIdx).Trim().ToLowerInvariant(); 57 | var value = line.Substring(colonIdx+1).Trim(); 58 | headers[name] = value; 59 | } 60 | 61 | return new Http1Request() 62 | { 63 | Method = method, 64 | Path = path, 65 | Protocol = proto, 66 | Headers = headers, 67 | }; 68 | } 69 | } 70 | 71 | /// 72 | /// A primitive HTTP/1 response parser 73 | /// 74 | class Http1Response 75 | { 76 | public string HttpVersion; 77 | public string StatusCode; 78 | public string Reason; 79 | 80 | private static Exception InvalidResponseHeaderException = 81 | new Exception("Invalid response header"); 82 | 83 | public Dictionary Headers = new Dictionary(); 84 | 85 | private static System.Text.RegularExpressions.Regex responseLineRegExp = 86 | new System.Text.RegularExpressions.Regex( 87 | @"([^\s]+) ([^\s]+) ([^\s]+)"); 88 | 89 | public static Http1Response ParseFrom(string responseHeader) 90 | { 91 | var lines = responseHeader.Split(new string[]{"\r\n"}, StringSplitOptions.None); 92 | if (lines.Length < 1) throw InvalidResponseHeaderException; 93 | 94 | // Parse request line in form GET /page HTTP/1.1 95 | // This is a super simple and bad parser 96 | 97 | var match = responseLineRegExp.Match(lines[0]); 98 | if (!match.Success) throw InvalidResponseHeaderException; 99 | 100 | var httpVersion = match.Groups[1].Value; 101 | var statusCode = match.Groups[2].Value; 102 | var reason = match.Groups[3].Value; 103 | if (string.IsNullOrEmpty(httpVersion) || 104 | string.IsNullOrEmpty(statusCode) || 105 | string.IsNullOrEmpty(reason)) 106 | throw InvalidResponseHeaderException; 107 | 108 | var headers = new Dictionary(); 109 | for (var i = 1; i < lines.Length; i++) 110 | { 111 | var line = lines[i]; 112 | var colonIdx = line.IndexOf(':'); 113 | if (colonIdx == -1) throw InvalidResponseHeaderException; 114 | var name = line.Substring(0, colonIdx).Trim().ToLowerInvariant(); 115 | var value = line.Substring(colonIdx+1).Trim(); 116 | headers[name] = value; 117 | } 118 | 119 | return new Http1Response() 120 | { 121 | HttpVersion = httpVersion, 122 | StatusCode = statusCode, 123 | Reason = reason, 124 | Headers = headers, 125 | }; 126 | } 127 | } -------------------------------------------------------------------------------- /Http2Tests/ConnectionPushPromiseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | using Http2; 9 | 10 | namespace Http2Tests 11 | { 12 | public class ConnectionPushPromiseTests 13 | { 14 | private readonly ILoggerProvider loggerProvider; 15 | 16 | public ConnectionPushPromiseTests(ITestOutputHelper outputHelper) 17 | { 18 | this.loggerProvider = new XUnitOutputLoggerProvider(outputHelper); 19 | } 20 | 21 | [Theory] 22 | [InlineData(true, 1, 0)] 23 | [InlineData(true, 1, 128)] 24 | [InlineData(true, 2, 0)] 25 | [InlineData(true, 2, 128)] 26 | [InlineData(false, 1, 0)] 27 | [InlineData(false, 1, 128)] 28 | [InlineData(false, 2, 0)] 29 | [InlineData(false, 2, 128)] 30 | public async Task ConnectionShouldGoAwayOnPushPromise( 31 | bool isServer, uint streamId, int payloadLength) 32 | { 33 | var inPipe = new BufferedPipe(1024); 34 | var outPipe = new BufferedPipe(1024); 35 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 36 | isServer, inPipe, outPipe, loggerProvider); 37 | 38 | var fh = new FrameHeader 39 | { 40 | Type = FrameType.PushPromise, 41 | Flags = 0, 42 | Length = payloadLength, 43 | StreamId = streamId, 44 | }; 45 | await inPipe.WriteFrameHeader(fh); 46 | await outPipe.AssertGoAwayReception(ErrorCode.ProtocolError, 0); 47 | await outPipe.AssertStreamEnd(); 48 | } 49 | 50 | [Theory] 51 | [InlineData(true)] 52 | [InlineData(false)] 53 | public async Task ConnectionShouldGoAwayOnPushPromiseWithInvalidStreamId( 54 | bool isServer) 55 | { 56 | var inPipe = new BufferedPipe(1024); 57 | var outPipe = new BufferedPipe(1024); 58 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 59 | isServer, inPipe, outPipe, loggerProvider); 60 | 61 | var fh = new FrameHeader 62 | { 63 | Type = FrameType.PushPromise, 64 | Flags = (byte)PushPromiseFrameFlags.EndOfHeaders, 65 | Length = 128, 66 | StreamId = 1, 67 | }; 68 | await inPipe.WriteFrameHeader(fh); 69 | await outPipe.AssertGoAwayReception(ErrorCode.ProtocolError, 0); 70 | await outPipe.AssertStreamEnd(); 71 | } 72 | 73 | [Theory] 74 | [InlineData(true, null)] 75 | [InlineData(true, 0)] 76 | [InlineData(true, 1)] 77 | [InlineData(true, 255)] 78 | [InlineData(false, null)] 79 | [InlineData(false, 0)] 80 | [InlineData(false, 1)] 81 | [InlineData(false, 255)] 82 | public async Task ConnectionShouldGoAwayOnPushPromiseWithInvalidLength( 83 | bool isServer, int? padLen) 84 | { 85 | var inPipe = new BufferedPipe(1024); 86 | var outPipe = new BufferedPipe(1024); 87 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 88 | isServer, inPipe, outPipe, loggerProvider); 89 | 90 | // This test is set up in a way where the payload length is 1 byte 91 | // too small for the transferred content 92 | var requiredLen = 4; 93 | var flags = (byte)PushPromiseFrameFlags.EndOfHeaders; 94 | if (padLen != null) 95 | { 96 | flags |= (byte)PushPromiseFrameFlags.Padded; 97 | requiredLen += 1 + padLen.Value; 98 | } 99 | var actualLen = requiredLen - 1; 100 | var fh = new FrameHeader 101 | { 102 | Type = FrameType.PushPromise, 103 | Flags = flags, 104 | Length = actualLen, 105 | StreamId = 1, 106 | }; 107 | await inPipe.WriteFrameHeader(fh); 108 | var content = new byte[actualLen]; 109 | var offset = 0; 110 | if (padLen != null) 111 | { 112 | content[offset] = (byte)padLen.Value; 113 | offset++; 114 | } 115 | // Set promised stream Id 116 | content[offset+0] = content[offset+1] = content[offset+2] = 0; 117 | if (offset+3 <= content.Length -1) 118 | { 119 | content[offset+3] = 1; 120 | } 121 | offset += 4; 122 | await inPipe.WriteAsync(new ArraySegment(content)); 123 | 124 | await outPipe.AssertGoAwayReception(ErrorCode.ProtocolError, 0); 125 | await outPipe.AssertStreamEnd(); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /HpackTests/StringEncoder.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using Http2.Hpack; 3 | 4 | namespace HpackTests 5 | { 6 | public class StringEncoderTests 7 | { 8 | [Fact] 9 | public void ShouldEncodeEmptyStringsWithoutHuffmanEncoding() 10 | { 11 | var testStr = ""; 12 | var bytes = StringEncoder.Encode(testStr, HuffmanStrategy.Never); 13 | Assert.Equal(1, bytes.Length); 14 | 15 | // Compare the bytes 16 | Assert.Equal(0x00, bytes[0]); 17 | } 18 | 19 | [Fact] 20 | public void ShouldEncodeEmptyStringsWithHuffmanEncoding() 21 | { 22 | var testStr = ""; 23 | var bytes = StringEncoder.Encode(testStr, HuffmanStrategy.Always); 24 | Assert.Equal(1, bytes.Length); 25 | 26 | // Compare the bytes 27 | Assert.Equal(0x80, bytes[0]); 28 | } 29 | 30 | [Fact] 31 | public void ShouldEncodeStringsWithoutHuffmanEncoding() 32 | { 33 | var testStr = "Hello World"; 34 | var bytes = StringEncoder.Encode(testStr, HuffmanStrategy.Never); 35 | Assert.Equal(12, bytes.Length); 36 | 37 | // Compare the bytes 38 | Assert.Equal(11, bytes[0]); 39 | for (var i = 0; i < testStr.Length; i++) 40 | { 41 | var c = testStr[i]; 42 | Assert.Equal((byte)c, bytes[1+i]); 43 | } 44 | 45 | // Test with a longer string 46 | testStr = ""; 47 | for (var i = 0; i < 64; i++) 48 | { 49 | testStr += "a"; 50 | } 51 | for (var i = 0; i < 64; i++) 52 | { 53 | testStr += "b"; 54 | } 55 | bytes = StringEncoder.Encode(testStr, HuffmanStrategy.Never); 56 | Assert.Equal(130, bytes.Length); 57 | 58 | // Compare the bytes 59 | Assert.Equal(127, bytes[0]); 60 | Assert.Equal(1, bytes[1]); 61 | for (var i = 0; i < testStr.Length; i++) 62 | { 63 | var c = testStr[i]; 64 | Assert.Equal((byte)c, bytes[2+i]); 65 | } 66 | } 67 | 68 | [Fact] 69 | public void ShouldEncodeStringsWithHuffmanEncoding() 70 | { 71 | var testStr = "Hello"; 72 | // 1100011 00101 101000 101000 00111 73 | // 11000110 01011010 00101000 00111 74 | // var expectedResult = 0xC65A283F; 75 | var bytes = StringEncoder.Encode(testStr, HuffmanStrategy.Always); 76 | Assert.Equal(5, bytes.Length); 77 | 78 | // Compare the bytes 79 | Assert.Equal(0x84, bytes[0]); 80 | Assert.Equal(0xC6, bytes[1]); 81 | Assert.Equal(0x5A, bytes[2]); 82 | Assert.Equal(0x28, bytes[3]); 83 | Assert.Equal(0x3F, bytes[4]); 84 | 85 | // Test with a longer string 86 | testStr = ""; 87 | for (var i = 0; i < 64; i++) 88 | { 89 | testStr += (char)9; // ffffea [24] 90 | testStr += "Z"; // fd [ 8] 91 | } 92 | 93 | bytes = StringEncoder.Encode(testStr, HuffmanStrategy.Always); 94 | Assert.Equal(3+4*64, bytes.Length); 95 | 96 | // Compare the bytes 97 | Assert.Equal(255, bytes[0]); // 127 98 | Assert.Equal(0x81, bytes[1]); // 127 + 1 = 128 99 | Assert.Equal(1, bytes[2]); // 128 + 128 = 256 100 | for (var i = 3; i < testStr.Length; i += 4) 101 | { 102 | Assert.Equal(0xFF, bytes[i+0]); 103 | Assert.Equal(0xFF, bytes[i+1]); 104 | Assert.Equal(0xEA, bytes[i+2]); 105 | Assert.Equal(0xFD, bytes[i+3]); 106 | } 107 | } 108 | 109 | [Fact] 110 | public void ShouldApplyHuffmanEncodingIfStringGetsSmaller() 111 | { 112 | var testStr = "test"; // 01001 00101 01000 01001 => 01001001 01010000 1001 113 | 114 | var bytes = StringEncoder.Encode(testStr, HuffmanStrategy.IfSmaller); 115 | Assert.Equal(4, bytes.Length); 116 | 117 | // Compare the bytes 118 | Assert.Equal(0x83, bytes[0]); 119 | Assert.Equal(0x49, bytes[1]); 120 | Assert.Equal(0x50, bytes[2]); 121 | Assert.Equal(0x9F, bytes[3]); 122 | } 123 | 124 | [Fact] 125 | public void ShouldNotApplyHuffmanEncodingIfStringDoesNotGetSmaller() 126 | { 127 | var testStr = "XZ"; // 11111100 11111101 128 | 129 | var bytes = StringEncoder.Encode(testStr, HuffmanStrategy.IfSmaller); 130 | Assert.Equal(3, bytes.Length); 131 | 132 | // Compare the bytes 133 | Assert.Equal(0x02, bytes[0]); 134 | Assert.Equal((byte)'X', bytes[1]); 135 | Assert.Equal((byte)'Z', bytes[2]); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Http2Tests/ConnectionPrefaceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | using Http2; 9 | 10 | namespace Http2Tests 11 | { 12 | public class ConnectionPrefaceTests 13 | { 14 | private Connection BuildConnection( 15 | bool isServer, 16 | IReadableByteStream inputStream, 17 | IWriteAndCloseableByteStream outputStream) 18 | { 19 | ILogger logger = null; 20 | if (loggerProvider != null) 21 | { 22 | logger = loggerProvider.CreateLogger("http2Con"); 23 | } 24 | 25 | // Decrease the timeout for the preface, 26 | // as this speeds the test up 27 | var config = 28 | new ConnectionConfigurationBuilder(isServer) 29 | .UseStreamListener((s) => false) 30 | .UseClientPrefaceTimeout(200) 31 | .Build(); 32 | 33 | return new Connection( 34 | config, inputStream, outputStream, 35 | new Connection.Options 36 | { 37 | Logger = logger, 38 | }); 39 | } 40 | 41 | private readonly ILoggerProvider loggerProvider; 42 | 43 | public ConnectionPrefaceTests(ITestOutputHelper outputHelper) 44 | { 45 | this.loggerProvider = new XUnitOutputLoggerProvider(outputHelper); 46 | } 47 | 48 | [Fact] 49 | public async Task ClientShouldSendPrefaceAtStartup() 50 | { 51 | var inPipe = new BufferedPipe(1024); 52 | var outPipe = new BufferedPipe(1024); 53 | var http2Con = BuildConnection(false, inPipe, outPipe); 54 | 55 | var b = new byte[ClientPreface.Length]; 56 | await outPipe.ReadAllWithTimeout(new ArraySegment(b)); 57 | Assert.Equal(ClientPreface.Bytes, b); 58 | } 59 | 60 | [Fact] 61 | public async Task ServerShouldCloseTheConnectionIfCorrectPrefaceIsNotReceived() 62 | { 63 | var inPipe = new BufferedPipe(1024); 64 | var outPipe = new BufferedPipe(1024); 65 | var http2Con = BuildConnection(true, inPipe, outPipe); 66 | 67 | var b = new byte[ClientPreface.Length]; 68 | // Initialize with non-preface data 69 | for (var i = 0; i < b.Length; i++) b[i] = 10; 70 | await inPipe.WriteAsync(new ArraySegment(b)); 71 | 72 | // Wait for the response - a settings frame is expected first 73 | // But as there's a race condition the connection could be closed 74 | // before or after the settings frame was fully received 75 | try 76 | { 77 | await outPipe.ReadAndDiscardSettings(); 78 | var hdrBuf = new byte[FrameHeader.HeaderSize + 50]; 79 | var header = await FrameHeader.ReceiveAsync(outPipe, hdrBuf); 80 | Assert.Equal(FrameType.GoAway, header.Type); 81 | } 82 | catch (Exception e) 83 | { 84 | Assert.IsType(e); 85 | } 86 | } 87 | 88 | [Theory] 89 | [InlineData(0)] 90 | [InlineData(1)] 91 | [InlineData(23)] 92 | public async Task ServerShouldCloseTheConnectionIfNoPrefaceIsSent( 93 | int nrDummyPrefaceData) 94 | { 95 | var inPipe = new BufferedPipe(1024); 96 | var outPipe = new BufferedPipe(1024); 97 | var http2Con = BuildConnection(true, inPipe, outPipe); 98 | 99 | // Write some dummy data 100 | // All this data is not long enough to be a preface, so the 101 | // preface reception should time out 102 | if (nrDummyPrefaceData != 0) 103 | { 104 | var b = new byte[nrDummyPrefaceData]; 105 | for (var i = 0; i < b.Length; i++) b[i] = 10; 106 | await inPipe.WriteAsync(new ArraySegment(b)); 107 | } 108 | 109 | // Settings will be sent by connection before the preface is 110 | // checked - so they must be discarded 111 | await outPipe.ReadAndDiscardSettings(); 112 | 113 | // Wait for the stream to end within 400ms. 114 | // This is longer than the timeout in the connection waiting for the 115 | // preface 116 | 117 | var buf = new byte[1]; 118 | var readTask = outPipe.ReadAsync(new ArraySegment(buf)).AsTask(); 119 | var timeoutTask = Task.Delay(400); 120 | var finishedTask = await Task.WhenAny( 121 | new Task[]{ readTask, timeoutTask }); 122 | if (ReferenceEquals(finishedTask, readTask)) 123 | { 124 | var res = readTask.Result; 125 | Assert.Equal(true, res.EndOfStream); 126 | Assert.Equal(0, res.BytesRead); 127 | // Received end of stream 128 | return; 129 | } 130 | Assert.True(false, 131 | "Expected connection to close outgoing stream. " + 132 | "Got timeout"); 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /Http2/Error.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Http2 4 | { 5 | /// 6 | /// Error codes that are standardized for HTTP/2 7 | /// 8 | public enum ErrorCode : uint 9 | { 10 | NoError = 0x0, 11 | ProtocolError = 0x1, 12 | InternalError = 0x2, 13 | FlowControlError = 0x3, 14 | SettingsTimeout = 0x4, 15 | StreamClosed = 0x5, 16 | FrameSizeError = 0x6, 17 | RefusedStream = 0x7, 18 | Cancel = 0x8, 19 | CompressionError = 0x9, 20 | ConnectError = 0xa, 21 | EnhanceYourCalm = 0xb, 22 | InadequateSecurity = 0xc, 23 | Http11Required = 0xd, 24 | } 25 | 26 | /// 27 | /// Carries information about an occured error 28 | /// 29 | public struct Http2Error 30 | { 31 | /// 32 | /// An HTTP/2 error code that will be transmitted to the remote in order 33 | /// to describe the error. 34 | /// 35 | public ErrorCode Code; 36 | 37 | /// 38 | /// The ID of the stream that is affected by the error. 39 | /// If the ID is 0 the error is a connection error. 40 | /// Otherwise it's a stream error. 41 | /// 42 | public uint StreamId; 43 | 44 | /// 45 | /// An optional message that further describes the error for logging 46 | /// purposes. 47 | /// 48 | public string Message; 49 | 50 | public override string ToString() 51 | { 52 | return $"Http2Error{{streamId={StreamId}, code={Code}, message=\"{Message}\"}}"; 53 | } 54 | } 55 | 56 | /// 57 | /// Extension methods for HTTP/2 error codes 58 | /// 59 | public static class ErrorCodeExtensions 60 | { 61 | private static readonly Dictionary Descriptions = 62 | new Dictionary 63 | { 64 | { 65 | ErrorCode.NoError, 66 | "The associated condition is not a result of an error. " + 67 | "For example, a GOAWAY might include this code to " + 68 | "indicate graceful shutdown of a connection" 69 | }, 70 | { 71 | ErrorCode.ProtocolError, 72 | "The endpoint detected an unspecific protocol error. " + 73 | "This error is for use when a more specific error code is not available" 74 | }, 75 | { 76 | ErrorCode.InternalError, 77 | "The endpoint encountered an unexpected internal error" 78 | }, 79 | { 80 | ErrorCode.FlowControlError, 81 | "The endpoint detected that its peer violated the " + 82 | "flow-control protocol" 83 | }, 84 | { 85 | ErrorCode.SettingsTimeout, 86 | "The endpoint sent a SETTINGS frame but did not receive " + 87 | "a response in a timely manner" 88 | }, 89 | { 90 | ErrorCode.StreamClosed, 91 | "The endpoint received a frame after a stream was half-closed" 92 | }, 93 | { 94 | ErrorCode.FrameSizeError, 95 | "The endpoint received a frame with an invalid size" 96 | }, 97 | { 98 | ErrorCode.RefusedStream, 99 | "The endpoint refused the stream prior to performing " + 100 | "any application processing" 101 | }, 102 | { 103 | ErrorCode.Cancel, 104 | "Used by the endpoint to indicate that the stream is " + 105 | "no longer needed" 106 | }, 107 | { 108 | ErrorCode.CompressionError, 109 | "The endpoint is unable to maintain the header " + 110 | "compression context for the connection" 111 | }, 112 | { 113 | ErrorCode.ConnectError, 114 | "The connection established in response to a CONNECT " + 115 | "request was reset or abnormally closed" 116 | }, 117 | { 118 | ErrorCode.EnhanceYourCalm, 119 | "The endpoint detected that its peer is exhibiting a " + 120 | "behavior that might be generating excessive load" 121 | }, 122 | { 123 | ErrorCode.InadequateSecurity, 124 | "The underlying transport has properties that do not " + 125 | "meet minimum security requirements" 126 | }, 127 | { 128 | ErrorCode.Http11Required, 129 | "The endpoint requires that HTTP/1.1 be used instead of " + 130 | "HTTP/2" 131 | }, 132 | }; 133 | 134 | /// 135 | /// Returns a human readable description for an HTTP/2 error code 136 | /// 137 | public static string Description(this ErrorCode code) 138 | { 139 | if (Descriptions.TryGetValue(code, out string desc)) return desc; 140 | return "Unknown error code " + code; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Http2/ClientUpgradeRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Buffers; 4 | using System.Threading.Tasks; 5 | using Http2.Hpack; 6 | 7 | namespace Http2 8 | { 9 | /// 10 | /// Represents the headers and payload of an upgrade request from 11 | /// HTTP/1.1 to HTTP/2 12 | /// 13 | public class ClientUpgradeRequest 14 | { 15 | /// 16 | /// The decoded settings that were included in the upgrade 17 | /// 18 | internal readonly Settings Settings; 19 | 20 | /// 21 | /// Returns the base64 encoded settings, which have to be sent inside 22 | /// Http2-Settings headerfield to the server during connection upgrade. 23 | /// 24 | public string Base64EncodedSettings => base64Settings; 25 | 26 | private readonly string base64Settings; 27 | private readonly bool valid; 28 | internal TaskCompletionSource UpgradeRequestStreamTcs 29 | = new TaskCompletionSource(); 30 | 31 | /// 32 | /// Returns whether the upgrade request from HTTP/1 to HTTP/2 is valid. 33 | /// If the upgrade is not valid an application must either reject the 34 | /// upgrade or send a HTTP/1 response. It may not try to create a HTTP/2 35 | /// connection based on that upgrade request. 36 | /// 37 | public bool IsValid => valid; 38 | 39 | /// 40 | /// Returns the stream which represents the initial upgraderequest. 41 | /// The server will send the HTTP/2 response to the HTTP/1.1 request 42 | /// which triggered the upgrade inside of this stream. 43 | /// The stream will only be available once the Connection has been fully 44 | /// established, therefore this property returns an awaitable Task. 45 | /// 46 | public Task UpgradeRequestStream => UpgradeRequestStreamTcs.Task; 47 | 48 | /// 49 | /// Constructs the ClientUpgradeRequest out ouf the information from 50 | /// the builder. 51 | /// 52 | internal ClientUpgradeRequest( 53 | Settings settings, 54 | string base64Settings, 55 | bool valid) 56 | { 57 | this.Settings = settings; 58 | this.base64Settings = base64Settings; 59 | this.valid = valid; 60 | } 61 | } 62 | 63 | /// 64 | /// A builder for ClientUpgradeRequests 65 | /// 66 | public class ClientUpgradeRequestBuilder 67 | { 68 | Settings settings = Settings.Default; 69 | 70 | /// 71 | /// Creates a new ClientUpgradeRequestBuilder 72 | /// 73 | public ClientUpgradeRequestBuilder() 74 | { 75 | settings.EnablePush = false; // Not available 76 | } 77 | 78 | /// 79 | /// Builds a ClientUpgradeRequest from the stored configuration. 80 | /// All other relevant configuration setter methods need to be called 81 | /// before. 82 | /// Only if the ClientUpgradeRequest.IsValid is true an upgrade 83 | /// to HTTP/2 may be performed. 84 | /// 85 | public ClientUpgradeRequest Build() 86 | { 87 | return new ClientUpgradeRequest( 88 | settings: settings, 89 | base64Settings: SettingsToBase64String(settings), 90 | valid: true); 91 | } 92 | 93 | /// 94 | /// Sets the HTTP/2 settings, which will be base64 encoded and must be 95 | /// sent together with the upgrade request in the Http2-Settings header. 96 | /// 97 | public ClientUpgradeRequestBuilder SetHttp2Settings(Settings settings) 98 | { 99 | if (!settings.Valid) 100 | throw new ArgumentException( 101 | "Can not use invalid settings", nameof(settings)); 102 | 103 | // Deactivate push - we do not support it and it will just cause 104 | // errors 105 | settings.EnablePush = false; 106 | this.settings = settings; 107 | return this; 108 | } 109 | 110 | /// 111 | /// Encode settings into base64 string 112 | /// 113 | private string SettingsToBase64String(Settings settings) 114 | { 115 | // Base64 encode the settings 116 | var settingsAsBytes = new byte[settings.RequiredSize]; 117 | settings.EncodeInto(new ArraySegment(settingsAsBytes)); 118 | var encodeBuf = new char[2*settings.RequiredSize]; 119 | var encodedLength = Convert.ToBase64CharArray( 120 | settingsAsBytes, 0, settingsAsBytes.Length, 121 | encodeBuf, 0); 122 | 123 | // Work around the fact that the standard .NET API seems to support 124 | // only normal base64 encoding, not base64url encoding which uses 125 | // different characters. 126 | for (var i = 0; i < encodedLength; i++) 127 | { 128 | if (encodeBuf[i] == '/') encodeBuf[i] = '_'; 129 | else if (encodeBuf[i] == '+') encodeBuf[i] = '-'; 130 | } 131 | 132 | return new string(encodeBuf, 0, encodedLength); 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /Examples/HttpsExampleServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Security; 7 | using System.Net.Sockets; 8 | using System.Reflection; 9 | using System.Security.Authentication; 10 | using System.Security.Cryptography.X509Certificates; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using Microsoft.Extensions.Logging; 15 | using Microsoft.Extensions.Logging.Console; 16 | using Http2; 17 | using Http2.Hpack; 18 | 19 | class Program 20 | { 21 | static void Main(string[] args) 22 | { 23 | var logProvider = new ConsoleLoggerProvider((s, level) => true, true); 24 | // Create a TCP socket acceptor 25 | var listener = new TcpListener(IPAddress.Any, 8889); 26 | listener.Start(); 27 | Task.Run(() => AcceptTask(listener, logProvider)).Wait(); 28 | } 29 | 30 | static bool AcceptIncomingStream(IStream stream) 31 | { 32 | Task.Run(() => HandleIncomingStream(stream)); 33 | return true; 34 | } 35 | 36 | static byte[] responseBody = Encoding.ASCII.GetBytes( 37 | "Hello WorldContent"); 38 | 39 | private static SslServerAuthenticationOptions options = new SslServerAuthenticationOptions() 40 | { 41 | // get our self signed certificate 42 | ServerCertificate = new X509Certificate2(ReadWholeStream(Assembly.GetExecutingAssembly() 43 | .GetManifestResourceStream("HttpsExampleServer.localhost.p12"))), 44 | // this line adds ALPN, critical for HTTP2 over SSL 45 | ApplicationProtocols = new List(){SslApplicationProtocol.Http2}, 46 | ClientCertificateRequired = false, 47 | CertificateRevocationCheckMode = X509RevocationMode.NoCheck, 48 | EnabledSslProtocols = SslProtocols.Tls12 49 | }; 50 | 51 | static byte[] ReadWholeStream(Stream stream) 52 | { 53 | byte[] buffer = new byte[16 * 1024]; 54 | using (MemoryStream ms = new MemoryStream()) 55 | { 56 | int read; 57 | while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) 58 | { 59 | ms.Write(buffer, 0, read); 60 | } 61 | 62 | return ms.ToArray(); 63 | } 64 | } 65 | 66 | static async void HandleIncomingStream(IStream stream) 67 | { 68 | try 69 | { 70 | // Read the headers 71 | var headers = await stream.ReadHeadersAsync(); 72 | var method = headers.First(h => h.Name == ":method").Value; 73 | var path = headers.First(h => h.Name == ":path").Value; 74 | // Print the request method and path 75 | Console.WriteLine("Method: {0}, Path: {1}", method, path); 76 | 77 | // Read the request body and write it to console 78 | var buf = new byte[2048]; 79 | while (true) 80 | { 81 | var readResult = await stream.ReadAsync(new ArraySegment(buf)); 82 | if (readResult.EndOfStream) break; 83 | // Print the received bytes 84 | Console.WriteLine(Encoding.ASCII.GetString(buf, 0, readResult.BytesRead)); 85 | } 86 | 87 | // Send a response which consists of headers and a payload 88 | var responseHeaders = new HeaderField[] 89 | { 90 | new HeaderField {Name = ":status", Value = "200"}, 91 | new HeaderField {Name = "content-type", Value = "text/html"}, 92 | }; 93 | await stream.WriteHeadersAsync(responseHeaders, false); 94 | await stream.WriteAsync(new ArraySegment( 95 | responseBody), true); 96 | 97 | // Request is fully handled here 98 | } 99 | catch (Exception e) 100 | { 101 | Console.WriteLine("Error during handling request: {0}", e.Message); 102 | stream.Cancel(); 103 | } 104 | } 105 | 106 | static async Task AcceptTask(TcpListener listener, ILoggerProvider logProvider) 107 | { 108 | var connectionId = 0; 109 | 110 | var settings = Settings.Default; 111 | settings.MaxConcurrentStreams = 50; 112 | 113 | var config = 114 | new ConnectionConfigurationBuilder(true) 115 | .UseStreamListener(AcceptIncomingStream) 116 | .UseSettings(settings) 117 | .UseHuffmanStrategy(HuffmanStrategy.IfSmaller) 118 | .Build(); 119 | 120 | while (true) 121 | { 122 | // Accept TCP sockets 123 | var clientSocket = await listener.AcceptSocketAsync(); 124 | clientSocket.NoDelay = true; 125 | // Create an SSL stream 126 | var sslStream = new SslStream(new NetworkStream(clientSocket, true)); 127 | // Authenticate on the stream 128 | await sslStream.AuthenticateAsServerAsync(options,CancellationToken.None); 129 | // wrap the SslStream 130 | var wrappedStreams = sslStream.CreateStreams(); 131 | // Build a HTTP connection on top of the stream abstraction 132 | var http2Con = new Connection( 133 | config, wrappedStreams.ReadableStream, wrappedStreams.WriteableStream, 134 | options: new Connection.Options 135 | { 136 | Logger = logProvider.CreateLogger("HTTP2Conn" + connectionId), 137 | }); 138 | 139 | // Close the connection if we get a GoAway from the client 140 | var remoteGoAwayTask = http2Con.RemoteGoAwayReason; 141 | var closeWhenRemoteGoAway = Task.Run(async () => 142 | { 143 | await remoteGoAwayTask; 144 | await http2Con.GoAwayAsync(ErrorCode.NoError, true); 145 | }); 146 | 147 | connectionId++; 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /Hpack/DynamicTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Http2.Hpack 5 | { 6 | /// 7 | /// A dynamic header table 8 | /// 9 | public class DynamicTable 10 | { 11 | private List _entries = new List(); 12 | 13 | /// The maximum size of the dynamic table 14 | private int _maxTableSize; 15 | 16 | /// The current used amount of bytes for the table 17 | private int _usedSize = 0; 18 | 19 | /// Get the current maximum size of the dynamic table 20 | public int MaxTableSize 21 | { 22 | get { return this._maxTableSize; } 23 | /// 24 | /// Sets a new maximum size of the dynamic table. 25 | /// The content will be evicted to fit into the new size. 26 | /// 27 | set 28 | { 29 | if (value >= this._maxTableSize) 30 | { 31 | this._maxTableSize = value; 32 | return; 33 | } 34 | 35 | // Table is shrinked, which means entries must be evicted 36 | this._maxTableSize = value; 37 | this.EvictTo(value); 38 | } 39 | } 40 | 41 | /// The size that is currently occupied by the table 42 | public int UsedSize => this._usedSize; 43 | 44 | /// 45 | /// Get the current length of the dynamic table 46 | /// 47 | public int Length => this._entries.Count; 48 | 49 | public TableEntry GetAt(int index) 50 | { 51 | if (index < 0 || index >= this._entries.Count) 52 | throw new IndexOutOfRangeException(); 53 | var elem = this._entries[index]; 54 | return elem; 55 | } 56 | 57 | public DynamicTable(int maxTableSize) 58 | { 59 | this._maxTableSize = maxTableSize; 60 | } 61 | 62 | private void EvictTo(int newSize) 63 | { 64 | if (newSize < 0) newSize = 0; 65 | // Delete as many entries as needed to conform to the new size 66 | // Start by counting how many entries need to be deleted 67 | var delCount = 0; 68 | var used = this._usedSize; 69 | var index = this._entries.Count - 1; // Start at end of the table 70 | while (used > newSize && index >= 0) 71 | { 72 | var item = this._entries[index]; 73 | used -= (32 + item.NameLen + item.ValueLen); 74 | index--; 75 | delCount++; 76 | } 77 | 78 | if (delCount == 0) return; 79 | else if (delCount == this._entries.Count) 80 | { 81 | this._entries.Clear(); 82 | this._usedSize = 0; 83 | } 84 | else 85 | { 86 | this._entries.RemoveRange(this._entries.Count - delCount, delCount); 87 | this._usedSize = used; 88 | } 89 | } 90 | 91 | /// 92 | /// Inserts a new element into the dynamic header table 93 | /// 94 | public bool Insert(string name, int nameBytes, string value, int valueBytes) 95 | { 96 | // Calculate the size that this dynamic table entry occupies according to the spec 97 | var entrySize = 32 + nameBytes + valueBytes; 98 | 99 | // Evict the dynamic table to have enough space for new entry - or to 0 100 | var maxUsedSize = this._maxTableSize - entrySize; 101 | if (maxUsedSize < 0) maxUsedSize = 0; 102 | this.EvictTo(maxUsedSize); 103 | 104 | // Return if entry doesn't fit into table 105 | if (entrySize > this._maxTableSize) return false; 106 | 107 | // Add the new entry at the beginning of the table 108 | var entry = new TableEntry 109 | { 110 | Name = name, 111 | NameLen = nameBytes, 112 | Value = value, 113 | ValueLen = valueBytes, 114 | }; 115 | this._entries.Insert(0, entry); 116 | this._usedSize += entrySize; 117 | return true; 118 | } 119 | 120 | /// 121 | /// Returns the index of the best matching element in the dynamic table. 122 | /// The index will be 0-based, means it is relative to the start of the 123 | /// dynamic table. 124 | /// If no index was found the return value is -1. 125 | /// If an index was found and the name as well as the value match 126 | /// isFullMatch will be set to true. 127 | /// 128 | public int GetBestMatchingIndex(HeaderField field, out bool isFullMatch) 129 | { 130 | var bestMatch = -1; 131 | isFullMatch = false; 132 | 133 | var i = 0; 134 | foreach (var entry in _entries) 135 | { 136 | if (entry.Name == field.Name) 137 | { 138 | if (bestMatch == -1) 139 | { 140 | // We used the lowest matching field index, which makes 141 | // search for the receiver the most efficient and provides 142 | // the highest chance to use the Static Table. 143 | bestMatch = i; 144 | } 145 | 146 | if (entry.Value == field.Value) 147 | { 148 | // It's a perfect match! 149 | isFullMatch = true; 150 | return i; 151 | } 152 | } 153 | i++; 154 | } 155 | 156 | return bestMatch; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Hpack/StaticTable.cs: -------------------------------------------------------------------------------- 1 | namespace Http2.Hpack 2 | { 3 | /// 4 | /// The static header table of HPACK as defined in RFC 7541 Appendix A 5 | /// 6 | public static class StaticTable 7 | { 8 | /// 9 | /// Returns the length of the static table 10 | /// 11 | public static int Length 12 | { 13 | get { return Entries.Length; } 14 | } 15 | 16 | /// 17 | /// Entries of the static header table 18 | /// 19 | public static readonly TableEntry[] Entries = 20 | { 21 | new TableEntry { Name = ":authority", NameLen = 10, Value = "", ValueLen = 0}, 22 | new TableEntry { Name = ":method", NameLen = 7, Value = "GET", ValueLen = 3}, 23 | new TableEntry { Name = ":method", NameLen = 7, Value = "POST", ValueLen = 4}, 24 | new TableEntry { Name = ":path", NameLen = 5, Value = "/", ValueLen = 1}, 25 | new TableEntry { Name = ":path", NameLen = 5, Value = "/index.html", ValueLen = 11}, 26 | new TableEntry { Name = ":scheme", NameLen = 7, Value = "http", ValueLen = 4}, 27 | new TableEntry { Name = ":scheme", NameLen = 7, Value = "https", ValueLen = 5}, 28 | new TableEntry { Name = ":status", NameLen = 7, Value = "200", ValueLen = 3}, 29 | new TableEntry { Name = ":status", NameLen = 7, Value = "204", ValueLen = 3}, 30 | new TableEntry { Name = ":status", NameLen = 7, Value = "206", ValueLen = 3}, 31 | new TableEntry { Name = ":status", NameLen = 7, Value = "304", ValueLen = 3}, 32 | new TableEntry { Name = ":status", NameLen = 7, Value = "400", ValueLen = 3}, 33 | new TableEntry { Name = ":status", NameLen = 7, Value = "404", ValueLen = 3}, 34 | new TableEntry { Name = ":status", NameLen = 7, Value = "500", ValueLen = 3}, 35 | new TableEntry { Name = "accept-charset", NameLen = 14, Value = "", ValueLen = 0}, 36 | new TableEntry { Name = "accept-encoding", NameLen = 15, Value = "gzip, deflate", ValueLen = 13}, 37 | new TableEntry { Name = "accept-language", NameLen = 15, Value = "", ValueLen = 0}, 38 | new TableEntry { Name = "accept-ranges", NameLen = 13, Value = "", ValueLen = 0}, 39 | new TableEntry { Name = "accept", NameLen = 6, Value = "", ValueLen = 0}, 40 | new TableEntry { Name = "access-control-allow-origin", NameLen = 27, Value = "", ValueLen = 0}, 41 | new TableEntry { Name = "age", NameLen = 3, Value = "", ValueLen = 0}, 42 | new TableEntry { Name = "allow", NameLen = 5, Value = "", ValueLen = 0}, 43 | new TableEntry { Name = "authorization", NameLen = 13, Value = "", ValueLen = 0}, 44 | new TableEntry { Name = "cache-control", NameLen = 13, Value = "", ValueLen = 0}, 45 | new TableEntry { Name = "content-disposition", NameLen = 19, Value = "", ValueLen = 0}, 46 | new TableEntry { Name = "content-encoding", NameLen = 16, Value = "", ValueLen = 0}, 47 | new TableEntry { Name = "content-language", NameLen = 16, Value = "", ValueLen = 0}, 48 | new TableEntry { Name = "content-length", NameLen = 14, Value = "", ValueLen = 0}, 49 | new TableEntry { Name = "content-location", NameLen = 16, Value = "", ValueLen = 0}, 50 | new TableEntry { Name = "content-range", NameLen = 13, Value = "", ValueLen = 0}, 51 | new TableEntry { Name = "content-type", NameLen = 12, Value = "", ValueLen = 0}, 52 | new TableEntry { Name = "cookie", NameLen = 6, Value = "", ValueLen = 0}, 53 | new TableEntry { Name = "date", NameLen = 4, Value = "", ValueLen = 0}, 54 | new TableEntry { Name = "etag", NameLen = 4, Value = "", ValueLen = 0}, 55 | new TableEntry { Name = "expect", NameLen = 6, Value = "", ValueLen = 0}, 56 | new TableEntry { Name = "expires", NameLen = 7, Value = "", ValueLen = 0}, 57 | new TableEntry { Name = "from", NameLen = 4, Value = "", ValueLen = 0}, 58 | new TableEntry { Name = "host", NameLen = 4, Value = "", ValueLen = 0}, 59 | new TableEntry { Name = "if-match", NameLen = 8, Value = "", ValueLen = 0}, 60 | new TableEntry { Name = "if-modified-since", NameLen = 17, Value = "", ValueLen = 0}, 61 | new TableEntry { Name = "if-none-match", NameLen = 13, Value = "", ValueLen = 0}, 62 | new TableEntry { Name = "if-range", NameLen = 8, Value = "", ValueLen = 0}, 63 | new TableEntry { Name = "if-unmodified-since", NameLen = 19, Value = "", ValueLen = 0}, 64 | new TableEntry { Name = "last-modified", NameLen = 13, Value = "", ValueLen = 0}, 65 | new TableEntry { Name = "link", NameLen = 4, Value = "", ValueLen = 0}, 66 | new TableEntry { Name = "location", NameLen = 8, Value = "", ValueLen = 0}, 67 | new TableEntry { Name = "max-forwards", NameLen = 12, Value = "", ValueLen = 0}, 68 | new TableEntry { Name = "proxy-authenticate", NameLen = 18, Value = "", ValueLen = 0}, 69 | new TableEntry { Name = "proxy-authorization", NameLen = 19, Value = "", ValueLen = 0}, 70 | new TableEntry { Name = "range", NameLen = 5, Value = "", ValueLen = 0}, 71 | new TableEntry { Name = "referer", NameLen = 7, Value = "", ValueLen = 0}, 72 | new TableEntry { Name = "refresh", NameLen = 7, Value = "", ValueLen = 0}, 73 | new TableEntry { Name = "retry-after", NameLen = 11, Value = "", ValueLen = 0}, 74 | new TableEntry { Name = "server", NameLen = 6, Value = "", ValueLen = 0}, 75 | new TableEntry { Name = "set-cookie", NameLen = 10, Value = "", ValueLen = 0}, 76 | new TableEntry { Name = "strict-transport-security", NameLen = 25, Value = "", ValueLen = 0}, 77 | new TableEntry { Name = "transfer-encoding", NameLen = 17, Value = "", ValueLen = 0}, 78 | new TableEntry { Name = "user-agent", NameLen = 10, Value = "", ValueLen = 0}, 79 | new TableEntry { Name = "vary", NameLen = 4, Value = "", ValueLen = 0}, 80 | new TableEntry { Name = "via", NameLen = 3, Value = "", ValueLen = 0}, 81 | new TableEntry { Name = "www-authenticate", NameLen = 16, Value = "", ValueLen = 0}, 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Hpack/StringEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace Http2.Hpack 5 | { 6 | /// 7 | /// Possible strategies for applying huffman encoding 8 | /// 9 | public enum HuffmanStrategy 10 | { 11 | /// Never use Huffman encoding 12 | Never, 13 | /// Always use Huffman encoding 14 | Always, 15 | /// Use Huffman if the string is shorter in huffman format 16 | IfSmaller, 17 | } 18 | 19 | /// 20 | /// Encodes string values according to the HPACK specification. 21 | /// 22 | public static class StringEncoder 23 | { 24 | /// 25 | /// Returns the byte length of the given string in non-huffman encoded 26 | /// form. 27 | /// 28 | public static int GetByteLength(string value) 29 | { 30 | return Encoding.ASCII.GetByteCount(value); 31 | } 32 | 33 | /// 34 | /// Encodes the given string into the target buffer 35 | /// 36 | /// The buffer into which the string should be serialized 37 | /// The value to encode 38 | /// 39 | /// The length of the string in bytes in non-huffman-encoded form. 40 | /// This can be retrieved through the GetByteLength method. 41 | /// 42 | /// Controls the huffman encoding 43 | /// 44 | /// The number of bytes that were required to encode the value. 45 | /// -1 if the value did not fit into the buffer. 46 | /// 47 | public static int EncodeInto( 48 | ArraySegment buf, 49 | string value, int valueByteLen, HuffmanStrategy huffman) 50 | { 51 | var offset = buf.Offset; 52 | var free = buf.Count; 53 | // Fast check for free space. Doesn't need to be exact 54 | if (free < 1 + valueByteLen) return -1; 55 | 56 | var encodedByteLen = valueByteLen; 57 | var requiredHuffmanBytes = 0; 58 | var useHuffman = huffman == HuffmanStrategy.Always; 59 | byte[] huffmanInputBuf = null; 60 | 61 | // Check if the string should be reencoded with huffman encoding 62 | if (huffman == HuffmanStrategy.Always || huffman == HuffmanStrategy.IfSmaller) 63 | { 64 | huffmanInputBuf = Encoding.ASCII.GetBytes(value); 65 | requiredHuffmanBytes = Huffman.EncodedLength( 66 | new ArraySegment(huffmanInputBuf)); 67 | if (huffman == HuffmanStrategy.IfSmaller && requiredHuffmanBytes < encodedByteLen) 68 | { 69 | useHuffman = true; 70 | } 71 | } 72 | 73 | if (useHuffman) 74 | { 75 | encodedByteLen = requiredHuffmanBytes; 76 | } 77 | 78 | // Write the required length to the target buffer 79 | var prefixContent = useHuffman ? (byte)0x80 : (byte)0; 80 | var used = IntEncoder.EncodeInto( 81 | new ArraySegment(buf.Array, offset, free), 82 | encodedByteLen, prefixContent, 7); 83 | if (used == -1) return -1; // Couldn't write length 84 | offset += used; 85 | free -= used; 86 | 87 | if (useHuffman) 88 | { 89 | if (free < requiredHuffmanBytes) return -1; 90 | // Use the huffman encoder to write bytes to target buffer 91 | used = Huffman.EncodeInto( 92 | new ArraySegment(buf.Array, offset, free), 93 | new ArraySegment(huffmanInputBuf)); 94 | if (used == -1) return -1; // Couldn't write length 95 | offset += used; 96 | } 97 | else 98 | { 99 | if (free < valueByteLen) return -1; 100 | // Use ASCII encoder to write bytes to target buffer 101 | used = Encoding.ASCII.GetBytes( 102 | value, 0, value.Length, buf.Array, offset); 103 | offset += used; 104 | } 105 | 106 | // Return the number amount of used bytes 107 | return offset - buf.Offset; 108 | } 109 | 110 | /// 111 | /// Encodes the given string. 112 | /// This method should not be directly used since it allocates. 113 | /// EncodeInto is preferred. 114 | /// 115 | /// The value to encode 116 | /// Controls the huffman encoding 117 | public static byte[] Encode(string value, HuffmanStrategy huffman) 118 | { 119 | // Estimate the size of the buffer 120 | var asciiSize = Encoding.ASCII.GetByteCount(value); 121 | var estimatedHeaderLength = IntEncoder.RequiredBytes(asciiSize, 0, 7); 122 | var estimatedBufferSize = estimatedHeaderLength + asciiSize; 123 | 124 | while (true) 125 | { 126 | // Create a buffer with some headroom 127 | var buf = new byte[estimatedBufferSize + 16]; 128 | // Try to serialize value in there 129 | var size = EncodeInto( 130 | new ArraySegment(buf), value, asciiSize, huffman); 131 | if (size != -1) 132 | { 133 | // Serialization was performed 134 | // Trim the buffer in order to return it 135 | if (size == buf.Length) return buf; 136 | var newBuf = new byte[size]; 137 | Array.Copy(buf, 0, newBuf, 0, size); 138 | return newBuf; 139 | } 140 | else 141 | { 142 | // Need more buffer space 143 | estimatedBufferSize = (estimatedBufferSize + 2) * 2; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Http2Tests/HeaderValidationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | using Http2; 9 | using Http2.Hpack; 10 | 11 | namespace Http2Tests 12 | { 13 | public class HeaderValidationTests 14 | { 15 | private ILoggerProvider loggerProvider; 16 | 17 | public HeaderValidationTests(ITestOutputHelper outHelper) 18 | { 19 | loggerProvider = new XUnitOutputLoggerProvider(outHelper); 20 | } 21 | 22 | [Fact] 23 | public async Task SendingInvalidHeadersShouldTriggerAStreamReset() 24 | { 25 | var inPipe = new BufferedPipe(1024); 26 | var outPipe = new BufferedPipe(1024); 27 | var headers = new HeaderField[] 28 | { 29 | new HeaderField { Name = "method", Value = "GET" }, 30 | new HeaderField { Name = ":scheme", Value = "http" }, 31 | new HeaderField { Name = ":path", Value = "/" }, 32 | }; 33 | 34 | Func listener = (s) => true; 35 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 36 | true, inPipe, outPipe, loggerProvider, listener); 37 | 38 | var hEncoder = new Encoder(); 39 | await inPipe.WriteHeaders(hEncoder, 1, false, headers); 40 | await outPipe.AssertResetStreamReception(1, ErrorCode.ProtocolError); 41 | } 42 | 43 | [Fact] 44 | public async Task SendingInvalidTrailersShouldTriggerAStreamReset() 45 | { 46 | var inPipe = new BufferedPipe(1024); 47 | var outPipe = new BufferedPipe(1024); 48 | var headers = new HeaderField[] 49 | { 50 | new HeaderField { Name = ":method", Value = "GET" }, 51 | new HeaderField { Name = ":scheme", Value = "http" }, 52 | new HeaderField { Name = ":path", Value = "/" }, 53 | }; 54 | var trailers = new HeaderField[] 55 | { 56 | new HeaderField { Name = ":method", Value = "GET" }, 57 | }; 58 | 59 | Func listener = (s) => true; 60 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 61 | true, inPipe, outPipe, loggerProvider, listener); 62 | 63 | var hEncoder = new Encoder(); 64 | await inPipe.WriteHeaders(hEncoder, 1, false, headers); 65 | await inPipe.WriteHeaders(hEncoder, 1, true, trailers); 66 | await outPipe.AssertResetStreamReception(1, ErrorCode.ProtocolError); 67 | } 68 | 69 | [Fact] 70 | public async Task RespondingInvalidHeadersShouldTriggerAnException() 71 | { 72 | var inPipe = new BufferedPipe(1024); 73 | var outPipe = new BufferedPipe(1024); 74 | 75 | var r = await ServerStreamTests.StreamCreator.CreateConnectionAndStream( 76 | StreamState.Open, loggerProvider, inPipe, outPipe); 77 | 78 | var headers = new HeaderField[] 79 | { 80 | new HeaderField { Name = "status", Value = "200" }, 81 | }; 82 | var ex = await Assert.ThrowsAsync(async () => 83 | await r.stream.WriteHeadersAsync(headers, false)); 84 | Assert.Equal("ErrorInvalidPseudoHeader", ex.Message); 85 | } 86 | 87 | [Fact] 88 | public async Task RespondingInvalidTrailersShouldTriggerAnException() 89 | { 90 | var inPipe = new BufferedPipe(1024); 91 | var outPipe = new BufferedPipe(1024); 92 | 93 | var r = await ServerStreamTests.StreamCreator.CreateConnectionAndStream( 94 | StreamState.Open, loggerProvider, inPipe, outPipe); 95 | 96 | var headers = new HeaderField[] 97 | { 98 | new HeaderField { Name = ":asdf", Value = "200" }, 99 | }; 100 | var ex = await Assert.ThrowsAsync(async () => 101 | await r.stream.WriteHeadersAsync(headers, false)); 102 | Assert.Equal("ErrorInvalidPseudoHeader", ex.Message); 103 | } 104 | 105 | [Theory] 106 | [InlineData(0, new int[]{ 0 }, false, false)] 107 | [InlineData(2, new int[]{ 2 }, false, false)] 108 | [InlineData(2, new int[]{ 2 }, true, false)] 109 | [InlineData(0, new int[]{ 1 }, false, true)] 110 | [InlineData(2, new int[]{ 1 }, false, true)] 111 | [InlineData(2, new int[]{ 1 }, true, true)] 112 | [InlineData(2, new int[]{ 3 }, false, true)] 113 | [InlineData(2, new int[]{ 1, 2 }, false, true)] 114 | [InlineData(2, new int[]{ 1, 2 }, true, true)] 115 | [InlineData(1024, new int[]{ 512, 513 }, false, true)] 116 | [InlineData(1024, new int[]{ 512, 513 }, true, true)] 117 | public async Task HeadersWithContentLengthShouldForceDataLengthValidation( 118 | int contentLen, int[] dataLength, bool useTrailers, bool shouldError) 119 | { 120 | var inPipe = new BufferedPipe(1024); 121 | var outPipe = new BufferedPipe(1024); 122 | 123 | Func listener = (s) => true; 124 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 125 | true, inPipe, outPipe, loggerProvider, listener); 126 | 127 | var headers = TestHeaders.DefaultGetHeaders.Append( 128 | new HeaderField { 129 | Name = "content-length", 130 | Value = contentLen.ToString() }); 131 | 132 | var hEncoder = new Encoder(); 133 | await inPipe.WriteHeaders(hEncoder, 1, false, headers); 134 | 135 | for (var i = 0; i < dataLength.Length; i++) 136 | { 137 | var isEos = i == (dataLength.Length - 1) && !useTrailers; 138 | await inPipe.WriteData(1u, dataLength[i], endOfStream: isEos); 139 | } 140 | 141 | if (useTrailers) 142 | { 143 | await inPipe.WriteHeaders(hEncoder, 1, true, new HeaderField[0]); 144 | } 145 | 146 | if (shouldError) 147 | { 148 | await outPipe.AssertResetStreamReception(1u, ErrorCode.ProtocolError); 149 | } 150 | else 151 | { 152 | await inPipe.WritePing(new byte[8], false); 153 | await outPipe.ReadAndDiscardPong(); 154 | } 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /Examples/BenchmarkServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Logging.Abstractions; 9 | using Http2; 10 | using Http2.Hpack; 11 | 12 | class Program 13 | { 14 | static void Main(string[] args) 15 | { 16 | if (args.Length > 0 && args[0] == "echo") 17 | { 18 | echoRequest = true; 19 | } 20 | 21 | var logProvider = NullLoggerProvider.Instance; 22 | // Create a TCP socket acceptor 23 | var listener = new TcpListener(IPAddress.Any, 8888); 24 | listener.Start(); 25 | Task.Run(() => AcceptTask(listener, logProvider)).Wait(); 26 | } 27 | 28 | static bool echoRequest = false; 29 | 30 | static bool AcceptIncomingStream(IStream stream) 31 | { 32 | Task.Run(() => 33 | { 34 | if (echoRequest) EchoHandler(stream); 35 | else DrainHandler(stream); 36 | }); 37 | return true; 38 | } 39 | 40 | static byte[] responseBody = Encoding.ASCII.GetBytes("Hello World!"); 41 | static byte[] emptyBody = new byte[0]; 42 | 43 | /// Echoes all request data back into the response 44 | static async void EchoHandler(IStream stream) 45 | { 46 | try 47 | { 48 | // Consume headers 49 | var headers = await stream.ReadHeadersAsync(); 50 | 51 | // Send response headers 52 | var responseHeaders = new HeaderField[] { 53 | new HeaderField { Name = ":status", Value = "200" }, 54 | new HeaderField { Name = "nextone", Value = "i am a header value" }, 55 | }; 56 | await stream.WriteHeadersAsync(responseHeaders, false); 57 | 58 | // Write request payload back into response 59 | await stream.CopyToAsync(stream); 60 | // Write end of stream 61 | await stream.WriteAsync(new ArraySegment(emptyBody), true); 62 | } 63 | catch (Exception e) 64 | { 65 | Console.WriteLine("Error during handling request: {0}", e.Message); 66 | stream.Cancel(); 67 | } 68 | } 69 | 70 | /// Drops the request data and writes a sucess response 71 | static async void DrainHandler(IStream stream) 72 | { 73 | try 74 | { 75 | // Consume headers 76 | var headers = await stream.ReadHeadersAsync(); 77 | 78 | // Read the request body to the end 79 | await stream.DrainAsync(); 80 | 81 | // Send a response which consists of headers and a payload 82 | var responseHeaders = new HeaderField[] { 83 | new HeaderField { Name = ":status", Value = "200" }, 84 | new HeaderField { Name = "nextone", Value = "i am a header value" }, 85 | }; 86 | await stream.WriteHeadersAsync(responseHeaders, false); 87 | await stream.WriteAsync(new ArraySegment(responseBody), true); 88 | } 89 | catch (Exception e) 90 | { 91 | Console.WriteLine("Error during handling request: {0}", e.Message); 92 | stream.Cancel(); 93 | } 94 | } 95 | 96 | static async Task AcceptTask(TcpListener listener, ILoggerProvider logProvider) 97 | { 98 | var connectionId = 0; 99 | 100 | var config = 101 | new ConnectionConfigurationBuilder(isServer: true) 102 | .UseStreamListener(AcceptIncomingStream) 103 | .UseHuffmanStrategy(HuffmanStrategy.Never) 104 | .UseBufferPool(Buffers.Pool) 105 | .Build(); 106 | 107 | while (true) 108 | { 109 | // Accept TCP sockets 110 | var clientSocket = await listener.AcceptSocketAsync(); 111 | clientSocket.NoDelay = true; 112 | // Create HTTP/2 stream abstraction on top of the socket 113 | var wrappedStreams = clientSocket.CreateStreams(); 114 | // Alternatively on top of a System.IO.Stream 115 | //var netStream = new NetworkStream(clientSocket, true); 116 | //var wrappedStreams = netStream.CreateStreams(); 117 | 118 | // Build a HTTP connection on top of the stream abstraction 119 | var http2Con = new Connection( 120 | config, wrappedStreams.ReadableStream, wrappedStreams.WriteableStream, 121 | options: new Connection.Options 122 | { 123 | Logger = logProvider.CreateLogger("HTTP2Conn" + connectionId), 124 | }); 125 | 126 | connectionId++; 127 | } 128 | } 129 | } 130 | 131 | public static class Buffers 132 | { 133 | public static ArrayPool Pool = ArrayPool.Create(64*1024, 200); 134 | } 135 | 136 | public static class RequestUtils 137 | { 138 | public async static Task DrainAsync(this IReadableByteStream stream) 139 | { 140 | var buf = Buffers.Pool.Rent(8*1024); 141 | var bytesRead = 0; 142 | 143 | try 144 | { 145 | while (true) 146 | { 147 | var res = await stream.ReadAsync(new ArraySegment(buf)); 148 | if (res.BytesRead != 0) 149 | { 150 | bytesRead += res.BytesRead; 151 | } 152 | 153 | if (res.EndOfStream) 154 | { 155 | return; 156 | } 157 | } 158 | } 159 | finally 160 | { 161 | Buffers.Pool.Return(buf); 162 | } 163 | } 164 | 165 | public async static Task CopyToAsync( 166 | this IReadableByteStream stream, 167 | IWriteableByteStream dest) 168 | { 169 | var buf = Buffers.Pool.Rent(64*1024); 170 | var bytesRead = 0; 171 | 172 | try 173 | { 174 | while (true) 175 | { 176 | var res = await stream.ReadAsync(new ArraySegment(buf)); 177 | if (res.BytesRead != 0) 178 | { 179 | await dest.WriteAsync(new ArraySegment(buf, 0, res.BytesRead)); 180 | bytesRead += res.BytesRead; 181 | } 182 | 183 | if (res.EndOfStream) 184 | { 185 | return; 186 | } 187 | } 188 | } 189 | finally 190 | { 191 | Buffers.Pool.Return(buf); 192 | } 193 | } 194 | } -------------------------------------------------------------------------------- /Http2/SocketExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Threading.Tasks; 4 | 5 | namespace Http2 6 | { 7 | /// 8 | /// Extension methods for System.Net.Sockets 9 | /// 10 | public static class SocketExtensions 11 | { 12 | /// 13 | /// Contains the result of a System.Net.Socket#CreateStreams operation 14 | /// 15 | public struct CreateStreamsResult 16 | { 17 | /// The resulting readable stream 18 | public IReadableByteStream ReadableStream; 19 | /// The resulting writable stream 20 | public IWriteAndCloseableByteStream WriteableStream; 21 | } 22 | 23 | /// 24 | /// Creates the required stream abstractions on top of a .NET 25 | /// Socket. 26 | /// The created stream wrappers will take ownership of the stream. 27 | /// It is not allowed to use the socket directly after this. 28 | /// 29 | public static CreateStreamsResult CreateStreams(this Socket socket) 30 | { 31 | if (socket == null) throw new ArgumentNullException(nameof(socket)); 32 | var wrappedStream = new SocketWrapper(socket); 33 | return new CreateStreamsResult 34 | { 35 | ReadableStream = wrappedStream, 36 | WriteableStream = wrappedStream, 37 | }; 38 | } 39 | 40 | internal class SocketWrapper : IReadableByteStream, IWriteAndCloseableByteStream 41 | { 42 | private Socket socket; 43 | /// 44 | /// Whether a speculative nonblocking read should by tried the next 45 | /// time instead of directly using an ReadAsync method. 46 | /// 47 | private bool tryNonBlockingRead = false; 48 | 49 | public SocketWrapper(Socket socket) 50 | { 51 | this.socket = socket; 52 | // Switch socket into nonblocking mode 53 | socket.Blocking = false; 54 | } 55 | 56 | public ValueTask ReadAsync(ArraySegment buffer) 57 | { 58 | if (buffer.Count == 0) 59 | { 60 | throw new Exception("Reading 0 bytes is not supported"); 61 | } 62 | 63 | var offset = buffer.Offset; 64 | var count = buffer.Count; 65 | 66 | if (tryNonBlockingRead) 67 | { 68 | // Try a nonblocking read if the last read yielded all required bytes 69 | // This means there are most likely bytes left in the socket buffer 70 | SocketError ec; 71 | var rcvd = socket.Receive(buffer.Array, offset, count, SocketFlags.None, out ec); 72 | if (ec != SocketError.Success && 73 | ec != SocketError.WouldBlock && 74 | ec != SocketError.TryAgain) 75 | { 76 | return new ValueTask( 77 | Task.FromException( 78 | new SocketException((int)ec))); 79 | } 80 | 81 | if (rcvd != count) 82 | { 83 | // Socket buffer seems empty 84 | // Use an async read next time 85 | tryNonBlockingRead = false; 86 | } 87 | 88 | if (ec == SocketError.Success) 89 | { 90 | return new ValueTask( 91 | new StreamReadResult 92 | { 93 | BytesRead = rcvd, 94 | EndOfStream = rcvd == 0, 95 | }); 96 | } 97 | 98 | // In the other case we got EAGAIN, which means we try 99 | // an async read now 100 | // Assert that we have nothing read yet - otherwise the 101 | // logic here would be broken 102 | if (rcvd != 0) 103 | { 104 | throw new Exception( 105 | "Unexpected reception of data in TryAgain case"); 106 | } 107 | } 108 | 109 | var readTask = socket.ReceiveAsync(buffer, SocketFlags.None); 110 | Task transformedTask = readTask.ContinueWith(tt => 111 | { 112 | if (tt.Exception != null) 113 | { 114 | throw tt.Exception; 115 | } 116 | 117 | var res = tt.Result; 118 | if (res == count) 119 | { 120 | // All required data was read 121 | // Try a speculative nonblocking read next time 122 | tryNonBlockingRead = true; 123 | } 124 | 125 | return new StreamReadResult 126 | { 127 | BytesRead = res, 128 | EndOfStream = res == 0, 129 | }; 130 | }); 131 | 132 | return new ValueTask(transformedTask); 133 | } 134 | 135 | public Task WriteAsync(ArraySegment buffer) 136 | { 137 | if (buffer.Count == 0) 138 | { 139 | return Task.CompletedTask; 140 | } 141 | 142 | // Try a nonblocking write first 143 | SocketError ec; 144 | var sent = socket.Send( 145 | buffer.Array, buffer.Offset, buffer.Count, SocketFlags.None, out ec); 146 | 147 | if (ec != SocketError.Success && 148 | ec != SocketError.WouldBlock && 149 | ec != SocketError.TryAgain) 150 | { 151 | throw new SocketException((int)ec); 152 | } 153 | 154 | if (sent == buffer.Count) 155 | { 156 | return Task.CompletedTask; 157 | } 158 | 159 | // More data needs to be sent 160 | var remaining = new ArraySegment( 161 | buffer.Array, buffer.Offset + sent, buffer.Count - sent); 162 | 163 | return socket.SendAsync(remaining, SocketFlags.None); 164 | } 165 | 166 | public Task CloseAsync() 167 | { 168 | socket.Dispose(); 169 | return Task.CompletedTask; 170 | } 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /Http2Tests/ClientUpgradeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | 12 | using Http2; 13 | using Http2.Hpack; 14 | using static Http2Tests.TestHeaders; 15 | 16 | namespace Http2Tests 17 | { 18 | public class ClientUpgradeTests 19 | { 20 | private ILoggerProvider loggerProvider; 21 | 22 | public ClientUpgradeTests(ITestOutputHelper outHelper) 23 | { 24 | loggerProvider = new XUnitOutputLoggerProvider(outHelper); 25 | } 26 | 27 | public static IEnumerable SettingsEncodings 28 | { 29 | get 30 | { 31 | var settings = Settings.Default; 32 | settings.EnablePush = false; 33 | 34 | yield return new object[]{ 35 | settings, "AAIAAAAAAAEAABAAAAQAAP__AAP_____AAUAAEAAAAb_____" 36 | }; 37 | 38 | settings.HeaderTableSize = 0x09080706; 39 | settings.EnablePush = false; // Will be reset by encoder anyway 40 | settings.MaxConcurrentStreams = 0x71727374; 41 | settings.InitialWindowSize = 0x10203040; 42 | settings.MaxFrameSize = 0x00332211; 43 | settings.MaxHeaderListSize = 0x01020304; 44 | yield return new object[]{ 45 | settings, "AAIAAAAAAAEJCAcGAAQQIDBAAANxcnN0AAUAMyIRAAYBAgME" 46 | }; 47 | } 48 | } 49 | 50 | [Theory] 51 | [MemberData(nameof(SettingsEncodings))] 52 | public void UpgradeRequestShouldBase64EncodeSettingsCorrectly( 53 | Settings settings, string expectedEncoding) 54 | { 55 | var upgrade = 56 | new ClientUpgradeRequestBuilder() 57 | .SetHttp2Settings(settings) 58 | .Build(); 59 | 60 | Assert.Equal(expectedEncoding, upgrade.Base64EncodedSettings); 61 | } 62 | 63 | [Fact] 64 | public async Task ClientUpgradeRequestShouldYieldStream1() 65 | { 66 | var inPipe = new BufferedPipe(1024); 67 | var outPipe = new BufferedPipe(1024); 68 | 69 | var upgrade = new ClientUpgradeRequestBuilder().Build(); 70 | var config = new ConnectionConfigurationBuilder(false) 71 | .Build(); 72 | 73 | var conn = new Connection( 74 | config, inPipe, outPipe, 75 | new Connection.Options 76 | { 77 | Logger = loggerProvider.CreateLogger("http2Con"), 78 | ClientUpgradeRequest = upgrade, 79 | }); 80 | 81 | await conn.PerformHandshakes(inPipe, outPipe); 82 | var stream = await upgrade.UpgradeRequestStream; 83 | Assert.Equal(1u, stream.Id); 84 | Assert.Equal(1, conn.ActiveStreamCount); 85 | Assert.Equal(StreamState.HalfClosedLocal, stream.State); 86 | 87 | var readHeadersTask = stream.ReadHeadersAsync(); 88 | Assert.False(readHeadersTask.IsCompleted); 89 | 90 | var hEncoder = new Http2.Hpack.Encoder(); 91 | await inPipe.WriteHeaders(hEncoder, 1u, false, DefaultStatusHeaders); 92 | Assert.True( 93 | await Task.WhenAny( 94 | readHeadersTask, 95 | Task.Delay(ReadableStreamTestExtensions.ReadTimeout)) 96 | == readHeadersTask, 97 | "Expected to read headers, got timeout"); 98 | var headers = await readHeadersTask; 99 | Assert.True(headers.SequenceEqual(DefaultStatusHeaders)); 100 | Assert.Equal(StreamState.HalfClosedLocal, stream.State); 101 | 102 | await inPipe.WriteData(1u, 100, 5, true); 103 | var data = await stream.ReadAllToArrayWithTimeout(); 104 | Assert.True(data.Length == 100); 105 | Assert.Equal(StreamState.Closed, stream.State); 106 | Assert.Equal(0, conn.ActiveStreamCount); 107 | } 108 | 109 | [Fact] 110 | public async Task TheNextOutgoingStreamAfterUpgradeShouldUseId3() 111 | { 112 | var inPipe = new BufferedPipe(1024); 113 | var outPipe = new BufferedPipe(1024); 114 | 115 | var upgrade = new ClientUpgradeRequestBuilder().Build(); 116 | var config = new ConnectionConfigurationBuilder(false) 117 | .Build(); 118 | 119 | var conn = new Connection( 120 | config, inPipe, outPipe, 121 | new Connection.Options 122 | { 123 | Logger = loggerProvider.CreateLogger("http2Con"), 124 | ClientUpgradeRequest = upgrade, 125 | }); 126 | 127 | await conn.PerformHandshakes(inPipe, outPipe); 128 | var stream = await upgrade.UpgradeRequestStream; 129 | Assert.Equal(1u, stream.Id); 130 | 131 | var readHeadersTask = stream.ReadHeadersAsync(); 132 | Assert.False(readHeadersTask.IsCompleted); 133 | 134 | var nextStream = await conn.CreateStreamAsync(DefaultGetHeaders); 135 | await outPipe.ReadAndDiscardHeaders(3u, false); 136 | Assert.Equal(3u, nextStream.Id); 137 | Assert.True(stream != nextStream); 138 | Assert.Equal(StreamState.HalfClosedLocal, stream.State); 139 | Assert.Equal(StreamState.Open, nextStream.State); 140 | 141 | var hEncoder = new Http2.Hpack.Encoder(); 142 | await inPipe.WriteHeaders(hEncoder, 3u, true, DefaultStatusHeaders); 143 | var nextStreamHeaders = await nextStream.ReadHeadersAsync(); 144 | Assert.True(nextStreamHeaders.SequenceEqual(DefaultStatusHeaders)); 145 | Assert.False(readHeadersTask.IsCompleted); 146 | Assert.Equal(StreamState.HalfClosedRemote, nextStream.State); 147 | Assert.Equal(StreamState.HalfClosedLocal, stream.State); 148 | Assert.Equal(2, conn.ActiveStreamCount); 149 | 150 | await nextStream.WriteAsync(new ArraySegment(new byte[0]), true); 151 | await outPipe.ReadAndDiscardData(3u, true, 0); 152 | Assert.Equal(StreamState.Closed, nextStream.State); 153 | Assert.Equal(1, conn.ActiveStreamCount); 154 | 155 | var headers2 = DefaultStatusHeaders.Append( 156 | new HeaderField(){ Name="hh", Value = "vv" }); 157 | await inPipe.WriteHeaders(hEncoder, 1u, false, headers2); 158 | var streamHeaders = await readHeadersTask; 159 | Assert.True(streamHeaders.SequenceEqual(headers2)); 160 | await inPipe.WriteData(1u, 10, 0, true); 161 | var data = await stream.ReadAllToArrayWithTimeout(); 162 | Assert.True(data.Length == 10); 163 | Assert.Equal(StreamState.Closed, stream.State); 164 | Assert.Equal(0, conn.ActiveStreamCount); 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /HpackTests/StringDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using Xunit; 4 | using Http2.Hpack; 5 | 6 | namespace HpackTests 7 | { 8 | public class StringDecoderTests 9 | { 10 | private ArrayPool bufPool = ArrayPool.Shared; 11 | 12 | [Fact] 13 | public void ShouldDecodeAnASCIIStringFromACompleteBuffer() 14 | { 15 | StringDecoder Decoder = new StringDecoder(1024, bufPool); 16 | 17 | // 0 Characters 18 | var buf = new Buffer(); 19 | buf.WriteByte(0x00); 20 | var consumed = Decoder.Decode(buf.View); 21 | Assert.True(Decoder.Done); 22 | Assert.Equal("", Decoder.Result); 23 | Assert.Equal(0, Decoder.StringLength); 24 | Assert.Equal(1, consumed); 25 | 26 | buf = new Buffer(); 27 | buf.WriteByte(0x04); // 4 Characters, non huffman 28 | buf.WriteByte('a'); 29 | buf.WriteByte('s'); 30 | buf.WriteByte('d'); 31 | buf.WriteByte('f'); 32 | 33 | consumed = Decoder.Decode(buf.View); 34 | Assert.True(Decoder.Done); 35 | Assert.Equal("asdf", Decoder.Result); 36 | Assert.Equal(4, Decoder.StringLength); 37 | Assert.Equal(5, consumed); 38 | 39 | // Multi-byte prefix 40 | buf = new Buffer(); 41 | buf.WriteByte(0x7F); // Prefix filled, non huffman, I = 127 42 | buf.WriteByte(0xFF); // I = 0x7F + 0x7F * 2^0 = 0xFE 43 | buf.WriteByte(0x03); // I = 0xFE + 0x03 * 2^7 = 0xFE + 0x180 = 0x27E = 638 44 | var expectedLength = 638; 45 | var expectedString = ""; 46 | for (var i = 0; i < expectedLength; i++) 47 | { 48 | buf.WriteByte(' '); 49 | expectedString += ' '; 50 | } 51 | consumed = Decoder.Decode(buf.View); 52 | Assert.True(Decoder.Done); 53 | Assert.Equal(expectedString, Decoder.Result); 54 | Assert.Equal(expectedLength, Decoder.StringLength); 55 | Assert.Equal(3 + expectedLength, consumed); 56 | } 57 | 58 | [Fact] 59 | public void ShouldDecodeAnASCIIStringIfPayloadIsInMultipleBuffers() 60 | { 61 | StringDecoder Decoder = new StringDecoder(1024, bufPool); 62 | 63 | // Only put the prefix in the first byte 64 | var buf = new Buffer(); 65 | buf.WriteByte(0x04); // 4 Characters, non huffman 66 | var consumed = Decoder.Decode(buf.View); 67 | Assert.False(Decoder.Done); 68 | Assert.Equal(1, consumed); 69 | 70 | // Next chunk with part of data 71 | buf = new Buffer(); 72 | buf.WriteByte('a'); 73 | buf.WriteByte('s'); 74 | consumed = Decoder.DecodeCont(buf.View); 75 | Assert.False(Decoder.Done); 76 | Assert.Equal(2, consumed); 77 | 78 | // Give the thing a depleted buffer 79 | consumed = Decoder.DecodeCont(new ArraySegment(buf.Bytes, 2, 0)); 80 | Assert.False(Decoder.Done); 81 | Assert.Equal(0, consumed); 82 | 83 | // Final chunk 84 | buf = new Buffer(); 85 | buf.WriteByte('d'); 86 | buf.WriteByte('f'); 87 | consumed = Decoder.DecodeCont(buf.View); 88 | Assert.True(Decoder.Done); 89 | Assert.Equal("asdf", Decoder.Result); 90 | Assert.Equal(4, Decoder.StringLength); 91 | Assert.Equal(2, consumed); 92 | } 93 | 94 | [Fact] 95 | public void ShouldDecodeAHuffmanEncodedStringIfLengthAndPayloadAreInMultipleBuffers() 96 | { 97 | StringDecoder Decoder = new StringDecoder(1024, bufPool); 98 | 99 | // Only put the prefix in the first byte 100 | var buf = new Buffer(); 101 | buf.WriteByte(0xFF); // Prefix filled, non huffman, I = 127 102 | var consumed = Decoder.Decode(buf.View); 103 | Assert.False(Decoder.Done); 104 | Assert.Equal(1, consumed); 105 | 106 | // Remaining part of the length plus first content byte 107 | buf = new Buffer(); 108 | buf.WriteByte(0x02); // I = 0x7F + 0x02 * 2^0 = 129 byte payload 109 | buf.WriteByte(0xf9); // first byte of the payload 110 | var expectedResult = "*"; 111 | consumed = Decoder.DecodeCont(buf.View); 112 | Assert.False(Decoder.Done); 113 | Assert.Equal(2, consumed); 114 | 115 | // Half of other content bytes 116 | buf = new Buffer(); 117 | for (var i = 0; i < 64; i = i+2) 118 | { 119 | expectedResult += ")-"; 120 | buf.WriteByte(0xfe); 121 | buf.WriteByte(0xd6); 122 | } 123 | consumed = Decoder.DecodeCont(buf.View); 124 | Assert.False(Decoder.Done); 125 | Assert.Equal(64, consumed); 126 | 127 | // Last part of content bytes 128 | buf = new Buffer(); 129 | for (var i = 0; i < 64; i = i+2) 130 | { 131 | expectedResult += "0+"; 132 | buf.WriteByte(0x07); 133 | buf.WriteByte(0xfb); 134 | } 135 | consumed = Decoder.DecodeCont(buf.View); 136 | Assert.True(Decoder.Done); 137 | Assert.Equal(expectedResult, Decoder.Result); 138 | Assert.Equal(129, Decoder.StringLength); 139 | Assert.Equal(64, consumed); 140 | } 141 | 142 | [Fact] 143 | public void ShouldCheckTheMaximumStringLength() 144 | { 145 | StringDecoder Decoder = new StringDecoder(2, bufPool); 146 | 147 | // 2 Characters are ok 148 | var buf = new Buffer(); 149 | buf.WriteByte(0x02); 150 | buf.WriteByte('a'); 151 | buf.WriteByte('b'); 152 | var consumed = Decoder.Decode(buf.View); 153 | Assert.True(Decoder.Done); 154 | Assert.Equal("ab", Decoder.Result); 155 | Assert.Equal(2, Decoder.StringLength); 156 | Assert.Equal(3, consumed); 157 | 158 | // 3 should fail 159 | buf = new Buffer(); 160 | buf.WriteByte(0x03); 161 | buf.WriteByte('a'); 162 | buf.WriteByte('b'); 163 | buf.WriteByte('c'); 164 | var ex = Assert.Throws(() => Decoder.Decode(buf.View)); 165 | Assert.Equal("Maximum string length exceeded", ex.Message); 166 | 167 | // Things were the length is stored in a continuation byte should also fail 168 | buf = new Buffer(); 169 | buf.WriteByte(0x7F); // More than 127 bytes 170 | consumed = Decoder.Decode(buf.View); 171 | Assert.False(Decoder.Done); 172 | Assert.Equal(1, consumed); 173 | buf.WriteByte(1); 174 | var view = new ArraySegment(buf.Bytes, 1, 1); 175 | ex = Assert.Throws(() => Decoder.DecodeCont(view)); 176 | Assert.Equal("Maximum string length exceeded", ex.Message); 177 | 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Http2/ConnectionConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using Http2.Hpack; 4 | 5 | namespace Http2 6 | { 7 | /// 8 | /// Configuration options for a Connnection. 9 | /// This class is immutable and can be shared between an arbitrary 10 | /// number of connections. 11 | /// 12 | public class ConnectionConfiguration 13 | { 14 | /// 15 | /// Whether the connection represents the client or server part of 16 | /// a HTTP/2 connection. True for servers. 17 | /// 18 | public readonly bool IsServer; 19 | 20 | /// 21 | /// The function that should be called whenever a new stream is 22 | /// opened by the remote peer. 23 | /// 24 | public readonly Func StreamListener; 25 | 26 | /// 27 | /// Strategy for applying huffman encoding on outgoing headers. 28 | /// 29 | public readonly HuffmanStrategy? HuffmanStrategy; 30 | 31 | /// 32 | /// The time to wait for a Client Preface on startup at server side. 33 | /// 34 | public readonly int ClientPrefaceTimeout; 35 | 36 | /// 37 | /// The HTTP/2 settings which will be utilized for the connection. 38 | /// 39 | public readonly Settings Settings; 40 | 41 | /// 42 | /// The buffer pool which will be utilized for allocating send and 43 | /// receive buffers. 44 | /// 45 | public readonly ArrayPool BufferPool; 46 | 47 | internal ConnectionConfiguration( 48 | bool isServer, 49 | Func streamListener, 50 | HuffmanStrategy? huffmanStrategy, 51 | Settings settings, 52 | ArrayPool bufferPool, 53 | int clientPrefaceTimeout) 54 | { 55 | this.IsServer = isServer; 56 | this.StreamListener = streamListener; 57 | this.HuffmanStrategy = huffmanStrategy; 58 | this.Settings = settings; 59 | this.BufferPool = bufferPool; 60 | this.ClientPrefaceTimeout = clientPrefaceTimeout; 61 | } 62 | } 63 | 64 | /// 65 | /// A builder for connection configurations. 66 | /// 67 | public class ConnectionConfigurationBuilder 68 | { 69 | private const int DefaultClientPrefaceTimeout = 1000; 70 | 71 | bool isServer = true; 72 | Func streamListener = null; 73 | HuffmanStrategy? huffmanStrategy = null; 74 | Settings settings = Settings.Default; 75 | ArrayPool bufferPool = null; 76 | int clientPrefaceTimeout = DefaultClientPrefaceTimeout; 77 | 78 | /// 79 | /// Creates a new Builder for connection configurations. 80 | /// 81 | /// 82 | /// Whether the connection is on the server side (true) or on the 83 | /// client side (false). 84 | /// 85 | public ConnectionConfigurationBuilder(bool isServer) 86 | { 87 | this.isServer = isServer; 88 | } 89 | 90 | /// 91 | /// Builds a ConnectionConfiguration from the configured connection 92 | /// settings. 93 | /// 94 | public ConnectionConfiguration Build() 95 | { 96 | if (isServer && streamListener == null) 97 | { 98 | throw new Exception( 99 | "Server connections must have configured a StreamListener"); 100 | } 101 | 102 | var pool = bufferPool; 103 | if (pool == null) pool = ArrayPool.Shared; 104 | 105 | var config = new ConnectionConfiguration( 106 | isServer: isServer, 107 | streamListener: streamListener, 108 | huffmanStrategy: huffmanStrategy, 109 | settings: settings, 110 | bufferPool: pool, 111 | clientPrefaceTimeout: clientPrefaceTimeout); 112 | 113 | return config; 114 | } 115 | 116 | /// 117 | /// Configures the function that should be called whenever a new stream 118 | /// is opened by the remote peer. This function must be configured for 119 | /// server configurations. 120 | /// The function should return true if it wants to handle the new 121 | /// stream and false otherwise. 122 | /// Applications should handle the stream in another Task. 123 | /// The Task from which this function is called may not be blocked. 124 | /// 125 | public ConnectionConfigurationBuilder UseStreamListener( 126 | Func streamListener) 127 | { 128 | this.streamListener = streamListener; 129 | return this; 130 | } 131 | 132 | /// 133 | /// Configures the strategy for applying huffman encoding on outgoing 134 | /// headers. 135 | /// 136 | public ConnectionConfigurationBuilder UseHuffmanStrategy(HuffmanStrategy strategy) 137 | { 138 | this.huffmanStrategy = strategy; 139 | return this; 140 | } 141 | 142 | /// 143 | /// Allows to override the HTTP/2 settings for the connection. 144 | /// If not explicitely specified the default HTTP/2 settings, 145 | /// which are stored within Settings.Default, will be utilized. 146 | /// 147 | public ConnectionConfigurationBuilder UseSettings(Settings settings) 148 | { 149 | if (!settings.Valid) 150 | { 151 | throw new Exception("Invalid settings"); 152 | } 153 | this.settings = settings; 154 | return this; 155 | } 156 | 157 | /// 158 | /// Configures a buffer pool which will be utilized for allocating send 159 | /// and receive buffers. 160 | /// If this is not explicitely configured ArrayPool<byte>.Shared 161 | /// will be used. 162 | /// 163 | /// The buffer pool to utilize 164 | public ConnectionConfigurationBuilder UseBufferPool(ArrayPool pool) 165 | { 166 | if (pool == null) throw new ArgumentNullException(nameof(pool)); 167 | this.bufferPool = pool; 168 | return this; 169 | } 170 | 171 | /// 172 | /// Allows to override the time time wait for a Client Preface on startup 173 | /// at server side. 174 | /// 175 | /// 176 | /// The time to wait for a client preface. 177 | /// The time must be configured in milliseconds and must be bigger than 0. 178 | /// 179 | public ConnectionConfigurationBuilder UseClientPrefaceTimeout(int timeout) 180 | { 181 | if (timeout <= 0) throw new ArgumentException(nameof(timeout)); 182 | this.clientPrefaceTimeout = timeout; 183 | return this; 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /Http2Tests/XUnitOutputLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.Extensions.Logging; 4 | 5 | using Xunit.Abstractions; 6 | 7 | namespace Http2Tests 8 | { 9 | /// 10 | /// A logger implementation that writes to the XUnit Test output 11 | /// 12 | public class XUnitOutputLogger : ILogger 13 | { 14 | private static readonly object _lock = new object(); 15 | private static readonly string _loglevelPadding = ": "; 16 | private static readonly string _newLineWithMessagePadding = Environment.NewLine + " "; 17 | 18 | private ITestOutputHelper _outputHelper; 19 | private string _name; 20 | private Func _filter; 21 | private bool _includeScopes; 22 | private System.Diagnostics.Stopwatch _sw; 23 | 24 | [ThreadStatic] 25 | private static StringBuilder _logBuilder; 26 | 27 | public XUnitOutputLogger( 28 | string name, Func filter, bool includeScopes, 29 | ITestOutputHelper outputHelper) 30 | { 31 | if (name == null) 32 | { 33 | throw new ArgumentNullException(nameof(name)); 34 | } 35 | 36 | _name = name; 37 | _filter = filter; 38 | _includeScopes = includeScopes; 39 | _outputHelper = outputHelper; 40 | _sw = new System.Diagnostics.Stopwatch(); 41 | _sw.Start(); 42 | } 43 | 44 | public void Log( 45 | LogLevel logLevel, 46 | EventId eventId, 47 | TState state, 48 | Exception exception, 49 | Func formatter) 50 | { 51 | if (!IsEnabled(logLevel)) 52 | { 53 | return; 54 | } 55 | 56 | if (formatter == null) 57 | { 58 | throw new ArgumentNullException(nameof(formatter)); 59 | } 60 | 61 | var message = formatter(state, exception); 62 | 63 | if (!string.IsNullOrEmpty(message) || exception != null) 64 | { 65 | WriteMessage(logLevel, _name, eventId.Id, message, exception); 66 | } 67 | } 68 | 69 | public virtual void WriteMessage( 70 | LogLevel logLevel, 71 | string logName, 72 | int eventId, 73 | string message, 74 | Exception exception) 75 | { 76 | var logBuilder = _logBuilder; 77 | _logBuilder = null; 78 | 79 | if (logBuilder == null) 80 | { 81 | logBuilder = new StringBuilder(); 82 | } 83 | 84 | // Example: 85 | // INFO: ConsoleApp.Program[10] 86 | // Request received 87 | if (!string.IsNullOrEmpty(message)) 88 | { 89 | logBuilder.Append(_sw.ElapsedMilliseconds); 90 | logBuilder.Append(" "); 91 | logBuilder.Append(GetLogLevelString(logLevel)); 92 | logBuilder.Append(_loglevelPadding); 93 | logBuilder.Append(logName); 94 | logBuilder.Append("["); 95 | logBuilder.Append(eventId); 96 | logBuilder.AppendLine("]"); 97 | if (_includeScopes) 98 | { 99 | GetScopeInformation(logBuilder); 100 | } 101 | var len = logBuilder.Length; 102 | logBuilder.Append(message); 103 | logBuilder.Replace( 104 | Environment.NewLine, _newLineWithMessagePadding, 105 | len, message.Length); 106 | } 107 | 108 | // Example: 109 | // System.InvalidOperationException 110 | // at Namespace.Class.Function() in File:line X 111 | if (exception != null) 112 | { 113 | // exception message 114 | if (!string.IsNullOrEmpty(message)) 115 | { 116 | logBuilder.AppendLine(); 117 | } 118 | logBuilder.Append(exception.ToString()); 119 | } 120 | 121 | if (logBuilder.Length > 0) 122 | { 123 | var logMessage = logBuilder.ToString(); 124 | lock (_lock) 125 | { 126 | _outputHelper.WriteLine(logMessage); 127 | } 128 | } 129 | 130 | logBuilder.Clear(); 131 | if (logBuilder.Capacity > 1024) 132 | { 133 | logBuilder.Capacity = 1024; 134 | } 135 | _logBuilder = logBuilder; 136 | } 137 | 138 | public bool IsEnabled(LogLevel logLevel) 139 | { 140 | if (_filter == null) return true; 141 | return _filter(_name, logLevel); 142 | } 143 | 144 | public class NullDisposeable : IDisposable 145 | { 146 | public void Dispose() 147 | { 148 | } 149 | 150 | public static IDisposable Instance { get; } = new NullDisposeable(); 151 | } 152 | 153 | public IDisposable BeginScope(TState state) 154 | { 155 | if (state == null) 156 | { 157 | throw new ArgumentNullException(nameof(state)); 158 | } 159 | 160 | return NullDisposeable.Instance; 161 | } 162 | 163 | private static string GetLogLevelString(LogLevel logLevel) 164 | { 165 | switch (logLevel) 166 | { 167 | case LogLevel.Trace: 168 | return "trce"; 169 | case LogLevel.Debug: 170 | return "dbug"; 171 | case LogLevel.Information: 172 | return "info"; 173 | case LogLevel.Warning: 174 | return "warn"; 175 | case LogLevel.Error: 176 | return "fail"; 177 | case LogLevel.Critical: 178 | return "crit"; 179 | default: 180 | throw new ArgumentOutOfRangeException(nameof(logLevel)); 181 | } 182 | } 183 | 184 | private void GetScopeInformation(StringBuilder builder) 185 | { 186 | } 187 | } 188 | 189 | public class XUnitOutputLoggerProvider : ILoggerProvider 190 | { 191 | private readonly ITestOutputHelper outputHelper; 192 | private int instanceId = 0; 193 | private static readonly bool XUnitLoggingEnabled = true; 194 | 195 | public XUnitOutputLoggerProvider(ITestOutputHelper outputHelper) 196 | { 197 | this.outputHelper = outputHelper; 198 | } 199 | 200 | public void Dispose() 201 | { 202 | } 203 | 204 | public ILogger CreateLogger(string categoryName) 205 | { 206 | if (XUnitLoggingEnabled) 207 | { 208 | var instId = System.Threading.Interlocked.Increment(ref instanceId); 209 | return new XUnitOutputLogger(categoryName, null, false, outputHelper); 210 | } 211 | return null; 212 | } 213 | } 214 | } -------------------------------------------------------------------------------- /HpackTests/IntDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using Http2.Hpack; 4 | 5 | namespace HpackTests 6 | { 7 | public class IntDecoderTests 8 | { 9 | [Fact] 10 | public void ShouldDecodeAValueThatIsCompletelyInThePrefix() 11 | { 12 | IntDecoder Decoder = new IntDecoder(); 13 | 14 | var buf = new Buffer(); 15 | buf.WriteByte(0x2A); 16 | buf.WriteByte(0x80); 17 | buf.WriteByte(0xFE); 18 | 19 | var consumed = Decoder.Decode(5, new ArraySegment(buf.Bytes, 0, 3)); 20 | Assert.True(Decoder.Done); 21 | Assert.Equal(10, Decoder.Result); 22 | Assert.Equal(1, consumed); 23 | 24 | consumed = Decoder.Decode(5, new ArraySegment(buf.Bytes, 1, 2)); 25 | Assert.True(Decoder.Done); 26 | Assert.Equal(0, Decoder.Result); 27 | Assert.Equal(1, consumed); 28 | 29 | consumed = Decoder.Decode(5, new ArraySegment(buf.Bytes, 2, 1)); 30 | Assert.True(Decoder.Done); 31 | Assert.Equal(30, Decoder.Result); 32 | Assert.Equal(1, consumed); 33 | 34 | // Test with 1bit prefix (least) 35 | buf = new Buffer(); 36 | buf.WriteByte(0xFE); 37 | buf.WriteByte(0x00); 38 | buf.WriteByte(0x54); 39 | 40 | consumed = Decoder.Decode(1, new ArraySegment(buf.Bytes, 0, 3)); 41 | Assert.True(Decoder.Done); 42 | Assert.Equal(0, Decoder.Result); 43 | Assert.Equal(1, consumed); 44 | 45 | consumed = Decoder.Decode(1, new ArraySegment(buf.Bytes, 1, 2)); 46 | Assert.True(Decoder.Done); 47 | Assert.Equal(0, Decoder.Result); 48 | Assert.Equal(1, consumed); 49 | 50 | consumed = Decoder.Decode(1, new ArraySegment(buf.Bytes, 2, 1)); 51 | Assert.True(Decoder.Done); 52 | Assert.Equal(0, Decoder.Result); 53 | Assert.Equal(1, consumed); 54 | 55 | // Test with 8bit prefix (largest) 56 | buf = new Buffer(); 57 | buf.WriteByte(0xFE); 58 | buf.WriteByte(0xEF); 59 | buf.WriteByte(0x00); 60 | buf.WriteByte(0x01); 61 | buf.WriteByte(0x2A); 62 | 63 | consumed = Decoder.Decode(8, new ArraySegment(buf.Bytes, 0, 5)); 64 | Assert.True(Decoder.Done); 65 | Assert.Equal(0xFE, Decoder.Result); 66 | Assert.Equal(1, consumed); 67 | 68 | consumed = Decoder.Decode(8, new ArraySegment(buf.Bytes, 1, 4)); 69 | Assert.True(Decoder.Done); 70 | Assert.Equal(0xEF, Decoder.Result); 71 | Assert.Equal(1, consumed); 72 | 73 | consumed = Decoder.Decode(8, new ArraySegment(buf.Bytes, 2, 3)); 74 | Assert.True(Decoder.Done); 75 | Assert.Equal(0, Decoder.Result); 76 | Assert.Equal(1, consumed); 77 | 78 | consumed = Decoder.Decode(8, new ArraySegment(buf.Bytes, 3, 2)); 79 | Assert.True(Decoder.Done); 80 | Assert.Equal(1, Decoder.Result); 81 | Assert.Equal(1, consumed); 82 | 83 | consumed = Decoder.Decode(8, new ArraySegment(buf.Bytes, 4, 1)); 84 | Assert.True(Decoder.Done); 85 | Assert.Equal(42, Decoder.Result); 86 | Assert.Equal(1, consumed); 87 | } 88 | 89 | [Fact] 90 | public void ShouldDecodeMultiByteValues() 91 | { 92 | IntDecoder Decoder = new IntDecoder(); 93 | 94 | var buf = new Buffer(); 95 | buf.WriteByte(0x1F); 96 | buf.WriteByte(0x9A); 97 | buf.WriteByte(10); 98 | var consumed = Decoder.Decode(5, new ArraySegment(buf.Bytes, 0, 3)); 99 | Assert.True(Decoder.Done); 100 | Assert.Equal(1337, Decoder.Result); 101 | Assert.Equal(3, consumed); 102 | } 103 | 104 | [Fact] 105 | public void ShouldDecodeInMultipleSteps() 106 | { 107 | IntDecoder Decoder = new IntDecoder(); 108 | 109 | var buf1= new Buffer(); 110 | buf1.WriteByte(0x1F); 111 | buf1.WriteByte(154); 112 | var buf2 = new Buffer(); 113 | buf2.WriteByte(10); 114 | 115 | var consumed = Decoder.Decode(5, buf1.View); 116 | Assert.False(Decoder.Done); 117 | Assert.Equal(2, consumed); 118 | 119 | consumed = Decoder.DecodeCont(buf2.View); 120 | Assert.True(Decoder.Done); 121 | Assert.Equal(1337, Decoder.Result); 122 | Assert.Equal(1, consumed); 123 | 124 | // And test with only prefix in first byte 125 | buf1 = new Buffer(); 126 | buf1.WriteByte(0x1F); 127 | buf2 = new Buffer(); 128 | buf2.WriteByte(154); 129 | buf2.WriteByte(10); 130 | 131 | consumed = Decoder.Decode(5, buf1.View); 132 | Assert.False(Decoder.Done); 133 | Assert.Equal(1, consumed); 134 | 135 | consumed = Decoder.DecodeCont(buf2.View); 136 | Assert.True(Decoder.Done); 137 | Assert.Equal(1337, Decoder.Result); 138 | Assert.Equal(2, consumed); 139 | 140 | // Test with a single bit prefix 141 | buf1 = new Buffer(); 142 | buf1.WriteByte(0xFF); // I = 1 143 | buf2 = new Buffer(); 144 | buf2.WriteByte(0x90); // I = 1 + 0x10 * 2^0 = 0x11 145 | buf2.WriteByte(0x10); // I = 0x81 + 0x10 * 2^7 = 0x11 + 0x800 = 0x811 146 | 147 | consumed = Decoder.Decode(1, buf1.View); 148 | Assert.False(Decoder.Done); 149 | Assert.Equal(1, consumed); 150 | 151 | consumed = Decoder.DecodeCont(buf2.View); 152 | Assert.True(Decoder.Done); 153 | Assert.Equal(0x811, Decoder.Result); 154 | Assert.Equal(2, consumed); 155 | 156 | // Test with 8bit prefix 157 | buf1 = new Buffer(); 158 | buf1.WriteByte(0xFF); // I = 0xFF 159 | buf1.WriteByte(0x90); // I = 0xFF + 0x10 * 2^0 = 0x10F 160 | buf2 = new Buffer(); 161 | buf2.WriteByte(0x10); // I = 0x10F + 0x10 * 2^7 = 0x10F + 0x800 = 0x90F 162 | 163 | consumed = Decoder.Decode(8, buf1.View); 164 | Assert.False(Decoder.Done); 165 | Assert.Equal(2, consumed); 166 | 167 | consumed = Decoder.DecodeCont(buf2.View); 168 | Assert.True(Decoder.Done); 169 | Assert.Equal(0x90F, Decoder.Result); 170 | Assert.Equal(1, consumed); 171 | } 172 | 173 | [Fact] 174 | public void ShouldThrowAnErrorIfDecodedValueGetsTooLarge() 175 | { 176 | IntDecoder Decoder = new IntDecoder(); 177 | 178 | // Add 5*7 = 35bits after the prefix, which results in a larger value than 2^53-1 179 | var buf = new Buffer(); 180 | buf.WriteByte(0x1F); 181 | buf.WriteByte(0xFF); 182 | buf.WriteByte(0xFF); 183 | buf.WriteByte(0xFF); 184 | buf.WriteByte(0xFF); 185 | buf.WriteByte(0xEF); 186 | var ex = Assert.Throws(() => Decoder.Decode(5, buf.View)); 187 | Assert.Equal(ex.Message, "invalid integer"); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Http2Tests/ConnectionCompletionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | using Http2; 8 | 9 | namespace Http2Tests 10 | { 11 | public class ConnectionCompletionTests 12 | { 13 | private ILoggerProvider loggerProvider; 14 | 15 | public ConnectionCompletionTests(ITestOutputHelper outHelper) 16 | { 17 | loggerProvider = new XUnitOutputLoggerProvider(outHelper); 18 | } 19 | 20 | [Theory] 21 | [InlineData(true)] 22 | [InlineData(false)] 23 | public async Task ConnectionShouldSignalDoneWhenInputIsClosed(bool isServer) 24 | { 25 | var inPipe = new BufferedPipe(1024); 26 | var outPipe = new BufferedPipe(1024); 27 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 28 | isServer, inPipe, outPipe, loggerProvider); 29 | 30 | await inPipe.CloseAsync(); 31 | // Expect the connection to close within timeout 32 | var closed = http2Con.Done; 33 | Assert.True( 34 | closed == await Task.WhenAny(closed, Task.Delay(1000)), 35 | "Expected connection to close"); 36 | } 37 | 38 | class FailingPipe 39 | : IWriteAndCloseableByteStream, IReadableByteStream, IBufferedPipe 40 | { 41 | public bool FailNextRead = false; 42 | public bool FailNextWrite = false; 43 | public bool CloseCalled = false; 44 | IBufferedPipe inner; 45 | 46 | public FailingPipe(IBufferedPipe inner) 47 | { 48 | this.inner = inner; 49 | } 50 | 51 | public Task WriteAsync(ArraySegment buffer) 52 | { 53 | if (!FailNextWrite) 54 | { 55 | return inner.WriteAsync(buffer); 56 | } 57 | return Task.FromException(new Exception("Write should fail")); 58 | } 59 | 60 | public Task CloseAsync() 61 | { 62 | this.CloseCalled = true; 63 | return inner.CloseAsync(); 64 | } 65 | 66 | public ValueTask ReadAsync(ArraySegment buffer) 67 | { 68 | if (!FailNextRead) 69 | { 70 | return inner.ReadAsync(buffer); 71 | } 72 | throw new Exception("Read should fail"); 73 | } 74 | } 75 | 76 | [Theory] 77 | [InlineData(true)] 78 | [InlineData(false)] 79 | public async Task ConnectionShouldCloseAndSignalDoneWhenReadingFromInputFails(bool isServer) 80 | { 81 | var inPipe = new BufferedPipe(1024); 82 | var outPipe = new BufferedPipe(1024); 83 | var failableInPipe = new FailingPipe(inPipe); 84 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 85 | isServer, failableInPipe, outPipe, loggerProvider); 86 | 87 | // Make the next write attempt fail 88 | failableInPipe.FailNextRead = true; 89 | // Send something which triggers no response but will start a new read call 90 | await inPipe.WriteWindowUpdate(0, 128); 91 | // Wait for the connection to close the outgoing part 92 | await outPipe.AssertStreamEnd(); 93 | // If the connection was successfully closed close the incoming data 94 | // stream, since this is expected from a bidirectional stream implementation 95 | await inPipe.CloseAsync(); 96 | // Expect the connection to close within timeout 97 | var closed = http2Con.Done; 98 | Assert.True( 99 | closed == await Task.WhenAny(closed, Task.Delay(1000)), 100 | "Expected connection to close"); 101 | } 102 | 103 | [Theory] 104 | [InlineData(true)] 105 | [InlineData(false)] 106 | public async Task ConnectionShouldCloseAndSignalDoneWhenWritingToOutputFails(bool isServer) 107 | { 108 | var inPipe = new BufferedPipe(1024); 109 | var outPipe = new BufferedPipe(1024); 110 | var failableOutPipe = new FailingPipe(outPipe); 111 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 112 | isServer, inPipe, failableOutPipe, loggerProvider); 113 | 114 | // Make the next write attempt fail 115 | failableOutPipe.FailNextWrite = true; 116 | // Send something which triggers a response 117 | await inPipe.WritePing(new byte[8], false); 118 | // Wait for the connection to close the outgoing part 119 | await outPipe.AssertStreamEnd(); 120 | Assert.True(failableOutPipe.CloseCalled); 121 | // If the connection was successfully closed close the incoming data 122 | // stream, since this is expected from a bidirectional stream implementation 123 | await inPipe.CloseAsync(); 124 | // Expect the connection to close within timeout 125 | var closed = http2Con.Done; 126 | Assert.True( 127 | closed == await Task.WhenAny(closed, Task.Delay(1000)), 128 | "Expected connection to close"); 129 | } 130 | 131 | [Theory] 132 | [InlineData(true)] 133 | [InlineData(false)] 134 | public async Task ConnectionShouldCloseAndSignalDoneInCaseOfAProtocolError(bool isServer) 135 | { 136 | var inPipe = new BufferedPipe(1024); 137 | var outPipe = new BufferedPipe(1024); 138 | var http2Con = await ConnectionUtils.BuildEstablishedConnection( 139 | isServer, inPipe, outPipe, loggerProvider); 140 | 141 | // Cause a protocol error 142 | var fh = new FrameHeader 143 | { 144 | Type = FrameType.Data, 145 | StreamId = 0u, 146 | Flags = 0, 147 | Length = 0, 148 | }; 149 | await inPipe.WriteFrameHeader(fh); 150 | await outPipe.AssertGoAwayReception(ErrorCode.ProtocolError, 0u); 151 | await outPipe.AssertStreamEnd(); 152 | await inPipe.CloseAsync(); 153 | 154 | // Expect the connection to close within timeout 155 | var closed = http2Con.Done; 156 | Assert.True( 157 | closed == await Task.WhenAny(closed, Task.Delay(1000)), 158 | "Expected connection to close"); 159 | } 160 | 161 | [Fact] 162 | public async Task ConnectionShouldCloseAndStreamsShouldGetResetWhenExternalCloseIsRequested() 163 | { 164 | // TODO: Add a variant of this test for clients as soon as they are supported 165 | var inPipe = new BufferedPipe(1024); 166 | var outPipe = new BufferedPipe(1024); 167 | 168 | var res = await ServerStreamTests.StreamCreator.CreateConnectionAndStream( 169 | StreamState.Open, loggerProvider, inPipe, outPipe); 170 | 171 | // Close the connection 172 | var closeTask = res.conn.CloseNow(); 173 | // Expect end of stream 174 | await outPipe.AssertStreamEnd(); 175 | // If the connection was successfully closed close the incoming data 176 | // stream, since this is expected from a bidirectional stream implementation 177 | await inPipe.CloseAsync(); 178 | // Close should now be completed 179 | await closeTask; 180 | // The stream should be reset 181 | Assert.Equal(StreamState.Reset, res.stream.State); 182 | // Which also means that further writes/reads should fail 183 | await Assert.ThrowsAsync(async () => 184 | { 185 | await res.stream.WriteHeadersAsync( 186 | TestHeaders.DefaultStatusHeaders, true); 187 | }); 188 | await Assert.ThrowsAsync(async () => 189 | { 190 | await res.stream.ReadAllToArray(); 191 | }); 192 | } 193 | } 194 | } --------------------------------------------------------------------------------