Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ public static void AddPrefix(string prefix, HttpListener listener)
private static void AddPrefixInternal(string p, HttpListener listener)
{
int start = p.IndexOf(':') + 3;
int colon = p.IndexOf(':', start);
int colon = ListenerPrefix.IndexOfPortSeparator(p.AsSpan(start));
if (colon != -1)
{
colon += start;

// root can't be -1 here, since we've already checked for ending '/' in ListenerPrefix.
int root = p.IndexOf('/', colon, p.Length - colon);
ReadOnlySpan<char> portString = p.AsSpan(colon + 1, root - colon - 1);
Expand All @@ -96,6 +98,10 @@ private static void AddPrefixInternal(string p, HttpListener listener)
if (lp.Host != "*" && lp.Host != "+" && Uri.CheckHostName(lp.Host) == UriHostNameType.Unknown)
throw new HttpListenerException((int)HttpStatusCode.BadRequest, SR.net_listener_host);

// IPv6 literals with a scope ID ("[fe80::1%25eth0]") are not supported.
if (lp.Host!.Contains('%'))
throw new HttpListenerException((int)HttpStatusCode.BadRequest, SR.net_listener_host);

if (lp.Path!.Contains('%'))
throw new HttpListenerException((int)HttpStatusCode.BadRequest, SR.net_invalid_path);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ internal void FinishInitialization()
if (raw_uri != null)
host = raw_uri.Host;

int colon = host.IndexOf(':');
int colon = ListenerPrefix.IndexOfPortSeparator(host);
if (colon >= 0)
host = host.Slice(0, colon);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,11 @@ private void Parse(string uri)
if (start_host >= length)
throw new ArgumentException(SR.net_listener_host);

int colon = uri.IndexOf(':', start_host, length - start_host);
int colon = IndexOfPortSeparator(uri.AsSpan(start_host));
int root;
if (colon > 0)
if (colon >= 0)
{
colon += start_host;
_host = uri.Substring(start_host, colon - start_host);
root = uri.IndexOf('/', colon, length - colon);
_port = (ushort)int.Parse(uri.AsSpan(colon + 1, root - colon - 1));
Expand All @@ -127,6 +128,24 @@ private void Parse(string uri)
}
if (_path.Length != 1)
_path = _path.Substring(0, _path.Length - 1);

// Requests are matched against Uri.Host, which writes an IPv6 literal in canonical form
// ("[::1]", lower-case hex), so store the prefix's literal the same way. A literal with a
// scope ID is left as written so that HttpEndPointManager can reject it.
if (_host.StartsWith('[') && !_host.Contains('%') && IPAddress.TryParse(_host, out IPAddress? address))
{
_host = $"[{address}]";
}
}

// Returns the index of the ':' that separates the host from the port in "host[:port]...",
// or -1 if there is none. An IPv6 literal ("[::1]") contains colons of its own, so for a
// host that starts with '[' the separator is the first ':' after the closing ']'.
internal static int IndexOfPortSeparator(ReadOnlySpan<char> hostAndPort)
{
int start = hostAndPort.StartsWith('[') ? Math.Max(hostAndPort.IndexOf(']'), 0) : 0;
int colon = hostAndPort.Slice(start).IndexOf(':');
return colon < 0 ? -1 : start + colon;
}
}
}
25 changes: 25 additions & 0 deletions src/libraries/System.Net.HttpListener/tests/GetContextHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,31 @@ public static class Helpers

private static bool TypeExists(string name) => typeof(HttpListener).Assembly.GetType(name, throwOnError: false, ignoreCase: false) != null;

// Whether a test can listen on http://[::1]:port/. Socket.OSSupportsIPv6 can be true on a host where ::1 cannot
// be bound (e.g. IPv6 disabled on the loopback interface), and http.sys may refuse to register an IP literal
// prefix for a process that is not elevated.
public static bool CanListenOnIPv6Loopback { get; } =
(IsManagedImplementation || PlatformDetection.IsPrivilegedProcess) && IsIPv6LoopbackAvailable();

private static bool IsIPv6LoopbackAvailable()
{
if (!Socket.OSSupportsIPv6)
{
return false;
}

try
{
using var socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.IPv6Loopback, 0));
return true;
}
catch (SocketException)
{
return false;
}
}

public static void WaitForSocketShutdown(Socket socket)
{
socket.Close();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace System.Net.Tests
{
[ActiveIssue("https://github.com/dotnet/runtime/issues/2391", TestRuntimes.Mono)]
[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] // httpsys component missing in Nano.
public class HttpListenerIPv6Tests
{
[ConditionalTheory(typeof(Helpers), nameof(Helpers.CanListenOnIPv6Loopback))]
[InlineData("[::1]")]
[InlineData("[0:0:0:0:0:0:0:1]")]
public async Task Prefix_IPv6Literal_ServesRequest(string literal)
{
using var factory = new HttpListenerFactory(literal);
using Socket client = factory.GetConnectedSocket();
string hostHeader = $"[::1]:{factory.Port}";

HttpListenerContext context = await GetContextAsync(factory.GetListener(), client,
$"GET /{factory.Path}/ HTTP/1.1\r\nHost: {hostHeader}\r\n\r\n");

Assert.Equal($"http://[::1]:{factory.Port}/{factory.Path}/", context.Request.Url.ToString());
Assert.Equal(hostHeader, context.Request.UserHostName);
Assert.Equal(IPAddress.IPv6Loopback, context.Request.LocalEndPoint.Address);
context.Response.Close();
}

[ConditionalTheory(typeof(Helpers), nameof(Helpers.CanListenOnIPv6Loopback))]
[InlineData("GET /{0}/ HTTP/1.1\r\nHost: [::1]:{1}\r\n\r\n")]
[InlineData("GET http://[::1]:{1}/{0}/ HTTP/1.1\r\nHost: [::1]:{1}\r\n\r\n")]
[InlineData("GET /{0}/ HTTP/1.0\r\n\r\n")]
public async Task Request_IPv6LiteralPrefix_ReturnsExpectedUrl(string requestFormat)
{
using var factory = new HttpListenerFactory("[::1]");
using Socket client = factory.GetConnectedSocket();

HttpListenerContext context = await GetContextAsync(factory.GetListener(), client,
string.Format(requestFormat, factory.Path, factory.Port));

Assert.Equal($"http://[::1]:{factory.Port}/{factory.Path}/", context.Request.Url.ToString());
context.Response.Close();
}

[ConditionalFact(typeof(HttpListenerFactory), nameof(HttpListenerFactory.SupportsWildcards))]
public async Task Request_IPv6LiteralHostHeaderToWildcardPrefix_ReturnsExpectedUrl()
{
using var factory = new HttpListenerFactory("+");
using Socket client = factory.GetConnectedSocket();
string hostHeader = $"[::1]:{factory.Port}";

HttpListenerContext context = await GetContextAsync(factory.GetListener(), client,
$"GET /{factory.Path}/ HTTP/1.1\r\nHost: {hostHeader}\r\n\r\n");

Assert.Equal($"http://[::1]:{factory.Port}/{factory.Path}/", context.Request.Url.ToString());
Assert.Equal(hostHeader, context.Request.UserHostName);
context.Response.Close();
}

private static async Task<HttpListenerContext> GetContextAsync(HttpListener listener, Socket client, string request)
{
Task<HttpListenerContext> getContext = listener.GetContextAsync();
await client.SendAsync(Encoding.ASCII.GetBytes(request));

// A request the listener rejects is answered on the socket and never reaches GetContextAsync.
byte[] buffer = new byte[4096];
Task<int> receive = client.ReceiveAsync(buffer.AsMemory(), SocketFlags.None).AsTask();
Task first = await Task.WhenAny(getContext, receive).WaitAsync(TimeSpan.FromSeconds(30));
if (first == receive)
{
Assert.Fail($"The listener answered the request itself: {Encoding.ASCII.GetString(buffer, 0, await receive)}");
}

return await getContext;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ public static IEnumerable<object[]> Hosts_TestData()
yield return new object[] { "*" };
yield return new object[] { "+" };
}

if (Helpers.CanListenOnIPv6Loopback)
{
yield return new object[] { "[::1]" };
}
}

[Theory]
Expand Down Expand Up @@ -352,6 +357,12 @@ public static IEnumerable<object[]> InvalidPrefix_TestData()
yield return new object[] { "http://localhost:65536/" };
yield return new object[] { "http://localhost:/" };
yield return new object[] { "http://localhost:trash/" };
yield return new object[] { "http://[::1]:-1/" };
yield return new object[] { "http://[::1]:0/" };
yield return new object[] { "http://[::1]:65536/" };
yield return new object[] { "http://[::1]:/" };
yield return new object[] { "http://[::1]:trash/" };
yield return new object[] { "http://[::1/" };
yield return new object[] { "http://localhost/invalid%path/" };
yield return new object[] { "http://./" };
yield return new object[] { "http://\\/" };
Expand Down Expand Up @@ -384,6 +395,33 @@ public void Add_InvalidPrefixAlreadyStarted_ThrowsHttpListenerExceptionOnAdd(str
}
}

[Theory]
[InlineData("http://[::1%1]:9200/")]
[InlineData("http://[::1%25lo]:9200/")]
public void Add_ScopedIPv6LiteralPrefix_ThrowsHttpListenerException(string uriPrefix)
{
using (var listener = new HttpListener())
{
listener.Prefixes.Add(uriPrefix);
AssertRejectedHost(Assert.Throws<HttpListenerException>(() => listener.Start()));
}

using (var factory = new HttpListenerFactory())
{
AssertRejectedHost(Assert.Throws<HttpListenerException>(() => factory.GetListener().Prefixes.Add(uriPrefix)));
}

static void AssertRejectedHost(HttpListenerException exception)
{
// The managed implementation rejects the host before it binds anything; a socket error code
// would mean the prefix got as far as binding.
if (Helpers.IsManagedImplementation)
{
Assert.Equal((int)HttpStatusCode.BadRequest, exception.ErrorCode);
}
}
}

[Theory]
[InlineData("")]
[InlineData("http")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Compile Include="HttpListenerFactory.cs" />
<Compile Include="HttpListenerAuthenticationTests.cs" />
<Compile Include="HttpListenerContextTests.cs" />
<Compile Include="HttpListenerIPv6Tests.cs" />
<Compile Include="HttpListenerPrefixCollectionTests.cs" />
<Compile Include="HttpListenerResponseTests.cs" />
<Compile Include="HttpListenerResponseTests.Cookies.cs" />
Expand Down
Loading