├── 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