Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Fix ipv6 address format in Host header #28578

Merged
merged 13 commits into from
Apr 2, 2018
Merged

Conversation

caesar-chen
Copy link
Contributor

@caesar-chen caesar-chen commented Mar 29, 2018

Per discussion in #28557. If this change looks ok, I will fix the WinHttp as well.

Fix: #28557 (moved to dotnet/runtime#25661)

using (HttpClientHandler handler = CreateHttpClientHandler())
using (var client = new HttpClient(handler))
{
handler.Proxy = new WebProxy(proxyUri);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you taught me this. : )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, ok, at least I'm consistent :)

[Fact]
public async Task GetAsync_IPv6AddressInHostHeader_CorrectlyFormatted()
{
string host = "[::1234]:8080";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make the test a theory and validate with and without a port?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

@@ -269,7 +269,8 @@ private void ConsumeFromRemainingBuffer(int bytesToConsume)
else
{
Debug.Assert(_pool.UsingProxy);
await WriteAsciiStringAsync(uri.IdnHost).ConfigureAwait(false);
await WriteAsciiStringAsync(uri.HostNameType == UriHostNameType.IPv6 ?
"[" + uri.IdnHost + "]" : uri.IdnHost).ConfigureAwait(false);
Copy link

@akashkc akashkc Mar 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using string interpolation instead of string concatenation manually i.e. $"[{uri.IdnHost}]"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String interpolation would be more expensive here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does string interpolation has significant performance difference than string concatenation. I believe, string interpolation has better readability.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case it'll compile down to a call to string.Concat:

string.Concat("[", uri.IdnHost, "]");

which will incur just a single allocation for the destination string, with the data from each constituent string blitted into it. In contrast, the string interpolation would compile down to the equivalent of:

string.Format("[{0}]", new object[] { uri.IdnHost });

and would thus incur at least the destination string and an object array allocation, plus the work that has to be done inside of Format, which involves a StringBuilder.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another useful note on string.Concat vs. string.Format #8025 (comment) (I want "bookmark favorite comment" feature in GitHub) :octocat:

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub : Thank you for explaining it with implementation side. @kasper3 : Thank you for commenting with interesting thread.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're now using uri.Host in the GetConnectionKey path, seems like it would make sense to use it here as well for consistency and to avoid the extra alloc (maybe?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, will change and add comment as well.

public async Task GetAsync_IPv6AddressInHostHeader_CorrectlyFormatted()
[Theory]
[InlineData("http://", "[::1234]")]
[InlineData("http://", "[::1234]:8080")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the scheme doesn't need to be parameterized

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

@caesar-chen
Copy link
Contributor Author

I was trying to add test data for https ws and wss as well, but those scheme will make the test to hang locally, not sure why.

[Theory]
[InlineData("http://", "[::1234]")]
[InlineData("http://", "[::1234]:8080")]
public async Task GetAsync_IPv6AddressInHostHeader_CorrectlyFormatted(string scheme, string host)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this test fails without your product change? Just checking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without product change:

        Assert.Contains() Failure
        Not found: Host: [::1234]
        In value:  List<String> ["GET http://::1234/ HTTP/1.1", "Host: ::1234"]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only semi-related, is the Uri we're sending to the proxy correctly formatted, or does it also need [] around the IPv6 address?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On WinHttpHandler, we will append [] around the IPv6 address.
"GET http://[::1234]/ HTTP/1.1", "Proxy-Connection: Keep-Alive", "Host: [::1234]"

On SocketsHttpHandler, currently we don't:
"GET http://::1234/ HTTP/1.1", "Host: [::1234]"

Per RFC (https://tools.ietf.org/html/rfc3986#section-3.2.2), I think we should do that for the URI we are sending as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found another issue that for SocketsHttpHandler, the port number is missing in uri sent to the proxy as well.

Opened #28609 to track this.

@caesar-chen
Copy link
Contributor Author

To provide more confidence, by stepping into the code, I can see in Host header, ipv6 address is appended with [] for the above hanging schemes. (since no test added for them)

@@ -269,7 +269,8 @@ private void ConsumeFromRemainingBuffer(int bytesToConsume)
else
{
Debug.Assert(_pool.UsingProxy);
await WriteAsciiStringAsync(uri.IdnHost).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the if block above? Don't we need to fix that case as well?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need to fix that as well. The fix there is more involved because it needs to happen as part of the construction of the connection key -- basically we should detect that the hostname is an IPv6 address when constructing the key, and construct the hostname using [].

You won't be able to test this using a proxy as you're doing here. But you should at least be able to test against IPv6Loopback. There's already a test for IP address based Uris -- GetAsync_IPBasedUri_Success. Unfortunately it's not validating the Host header. It should be modified to validate the Host header. And it would be nice to add https cases to that test as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out! I will make a fix and add tests to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the above if by changing GetConnectionKey method.

We still need to special case this else statement for Proxy case, because for this case, we pass null as host, thus _pool.HostHeaderValueBytes == null, and here is the only place to append []

@geoffkizer
Copy link

I was trying to add test data for https ws and wss as well, but those scheme will make the test to hang locally, not sure why.

That's because those schemes will try to tunnel through your proxy.

@caesar-chen
Copy link
Contributor Author

New commit explanation:

Fix the IPv6 missing [ ] in GetConnectionKey() method. However, there is a special case, for Proxy state, we pass null as host, thus _pool.HostHeaderValueBytes == null. So we need to fix this case in WriteHostHeaderAsync() method, as the previous approach does. So I didn't change that.

@caesar-chen
Copy link
Contributor Author

@dotnet-bot test Linux x64 Release Build
@dotnet-bot test OSX x64 Debug Build

CI hang.

@@ -140,11 +140,12 @@ private static string ParseHostNameFromHeader(string hostHeader)
private static HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri proxyUri, bool isProxyConnect)
{
Uri uri = request.RequestUri;
string host = uri.HostNameType == UriHostNameType.IPv6 ? "[" + uri.IdnHost + "]" : uri.IdnHost;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is adding an allocation for every request using an IPv6 address as a host name. Is that necessary? Since the resulting HttpConnectionKey is used to choose the pool to use, can't this be cached on the pool instead? And if not, can we skip doing this if proxyUri == null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't this be cached on the pool instead?

I think it's possible. I didn't do that in the first place, because we will need a helper logic to determine if the address we passed in is IPv6 or not (we pass in string, not uri), and I thought the helper logic could result in additional allocation. I will try to explore this option, if not able to come up with efficient way, we may need to try the second appraoch -> skip doing this if proxyUri == null.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IPv6 host names are unusual, saving a few extra allocations for them is not a high priority.

@caesar-chen
Copy link
Contributor Author

@stephentoub Do you think this change will result in smaller allocation?

@@ -117,12 +117,15 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK

if (_host != null)
{
bool isHostTypeIPv6 = _host.Split(':').Length - 1 > 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be just _host.Contains(':')? Why does it check for 2+ :?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A valid IPv6 address contains at least two :, so I check it for two times.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split allocates unnecessarily - even if just per ConnectionPool, what about using IndexOf on the string twice?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's performance critical, then I think more efficient way is:

var count = 0;
var isHostTypeIPv6 = false;
foreach (char c in _host)
{
  if (c == ':' && ++count > 1)
  {
    isHostTypeIPv6 = true;
    break;
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider host is not a long string, IndexOf can make the code cleaner.

from useSsl in new[] { true, false }
select new object[] { address, useSsl };

[OuterLoop]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add to outerloop since the execution for this test is long, will cause Linux catastrophic failure in innerloop run.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the text execution long?

Why does it cause catastrophic failure on Linux?

This seems like a straightforward test, what am I missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Linux catastrophic failure confused me a lot as well. Here is my observation:

  1. Before adding this test, the CI is green.
  2. After I added the test, it will consistently (every time ) cause catastrophic failure on Centos, Fedora, RedHat, and Ubuntu 1804, while passing on other distros. No matter I put it in innerloop/outerloop.
  3. After I disable the test on CurlHandler, no catastrophic failure with System.Net.Http. (There is one with System.Runtime.Extensions.Tests, I think it's totally unrelated).

Why is the text execution long?

That's my guess. The execution log didn't show any test failure. So I doubt the execution failed is because of hang, then timeout.

Why does it cause catastrophic failure on Linux?

The test consistently cause catastrophic failure on Centos, Fedora, RedHat, and Ubuntu 1804, while passing on other distros. I think it could be (very likely) different CurlHandler version causing the issue. (Since Windows run is green every time).

To summary: I think it's a strange issue with CurlHandler causing the catastrophic failure. To unblock the PR, I will open a new issue to tract this, and disable the test against CurlHandler.

@@ -117,12 +117,16 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK

if (_host != null)
{
int posColon = -1;
bool isHostTypeIPv6 = (-1 != (posColon = _host.IndexOf(':')) && -1 != (posColon = _host.IndexOf(':', posColon)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the second _host.IndexOf always going to succeed, because you're passing in posColon rather than posColon+1? Seems like we need some more tests, too, to catch this kind of thing.

Is it true that every IPv6 address contains at least two colons, and that if an address contains two colons it must be an IPv6 address? If so, with the aforementioned change, the logic seems fine.

If it's not true, a better approach might be to augment the HttpConnectionPool ctor to accept a "Uri exemplarUri", then at the ctor call site pass in the request.Uri, and here use Uri.HostNameType instead of doing this check manually.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, every ipv6 contains at least two colons and nothing else does.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then with the index fix, this looks good, e.g.

bool isHostTypeIPv6 = (posColon = _host.IndexOf(':')) != -1 && _host.IndexOf(':', posColon+1) != -1;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

@caesar-chen
Copy link
Contributor Author

The new test I added: GetAsync_SecureAndNonSecureIPBasedUri_CorrectlyFormatted will consistently causing catastrophic failure on Centos, Fedora, RedHat, and Ubuntu 1804, while passing on other distros. I think it could be different CurlHandler version causing the issue. (Since Windows run is green every time).

I will disable the test against Linux, to see if this is the issue.

@caesar-chen
Copy link
Contributor Author

@dotnet-bot test Outerloop Windows x64 Debug Build
@dotnet-bot test Outerloop Linux x64 Debug Build

int posColon = -1;
bool isHostTypeIPv6 = (posColon = _host.IndexOf(':')) != -1 && _host.IndexOf(':', posColon+1) != -1;
string hostAddress = isHostTypeIPv6 ? "[" + _host + "]" : _host;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use Uri.CheckHostName here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, can we just go back to checking for HostNameType == IPv6 in GetConnectionKey, but use uri.Host instead of uri.IdnHost in that case? I think that will return the address with [], right?

I'm kinda queasy about doing our own parsing here. Uri does all sorts of stuff for us already, let's just use it if possible instead of trying to duplicate the logic ourselves.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will also point out that retrieving parts of the Uri (e.g. hostname) is already a bit of a perf bottleneck. Uri.IdnHost is allocating a string. I believe we also end up allocating later when we retrieve path/query etc.

We should look at optimizing this better, but not for 2.1.

In the meantime let's just do something simple and correct, and we can optimize interaction with Uri in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just go back to checking for HostNameType == IPv6 in GetConnectionKey, but use uri.Host instead of uri.IdnHost in that case?

WinHttp clearly documented that it will only accept Punycode host name. Changing to uri.Host may bring regression for SocketsHttpHandler, and I think that's risky for 2.1.

I think that will return the address with [], right?

Yes, uri.Host will return the address with [].

I'm kinda queasy about doing our own parsing here.

I will change to Uri.CheckHostName.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WinHttp clearly documented that it will only accept Punycode host name. Changing to uri.Host may bring regression for SocketsHttpHandler, and I think that's risky for 2.1.

I'm not suggesting we change it in all cases. Obviously that would break Punycode handling. Just in the case where it's an IP address.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should work. I will push a new commit.


if (isProxyConnect)
{
Debug.Assert(uri == proxyUri);
return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, uri.IdnHost, uri.Port, null, proxyUri);
return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, isIPv6Address ? uri.Host: uri.IdnHost, uri.Port, null, proxyUri);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: space before colon in conditional expression, here and below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops T.T, will fix.

@@ -140,11 +140,12 @@ private static string ParseHostNameFromHeader(string hostHeader)
private static HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri proxyUri, bool isProxyConnect)
{
Uri uri = request.RequestUri;
bool isIPv6Address = uri.HostNameType == UriHostNameType.IPv6;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a comment here, something like:

// If the hostname is an IPv6 address, uri.IdnHost will return the address without enclosing [].
// In this case, use uri.Host instead, which will correctly enclose with [].
// Note we don't need punycode encoding if it's an IP address, so using uri.Host is fine.

{
if (useSsl)
{
handler.ServerCertificateCustomValidationCallback = delegate { return true; };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned in a separate issue that you were seeing this test hang with CurlHandler on some OSes. Have you tried using HttpClientHandler.DangerousAcceptAnyServerCertificateValidator here instead of delegate { return true; }?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I didn't. I will try it and enable the test to see if this can resolve the hang.

if (useSsl)
{
handler.ServerCertificateCustomValidationCallback = delegate { return true; };
handler.ClientCertificateOptions = ClientCertificateOption.Automatic;
Copy link
Member

@stephentoub stephentoub Apr 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this setting of ClientCertificateOptions needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove.

@caesar-chen
Copy link
Contributor Author

@stephentoub

Changing to HttpClientHandler.DangerousAcceptAnyServerCertificateValidator resolved the Linux catastrophic failure. Close #28703.

@dotnet-bot test NETFX x86 Release Build

handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
handler.ServerCertificateCustomValidationCallback =
#if netcoreapp
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use TestHelper.AllowAllCertificates

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will change.

@caesar-chen
Copy link
Contributor Author

Unrelated failure.

@dotnet-bot test Linux x64 Release Build
@dotnet-bot test OSX x64 Debug Build

@caesar-chen caesar-chen merged commit 24889e2 into dotnet:master Apr 2, 2018
@caesar-chen caesar-chen deleted the ipv6_host branch April 2, 2018 18:26
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
* fix ipv6 format in host header

* address feedback

* address feedback

* fix common case

* reduce allocation

* address feedback

* fix indexof issue

* disable on curlhandler

* revert to simple fix

* address feedback and add comment

* address test issue, enable on curlhandler

* fix framework failure

* address feedback


Commit migrated from dotnet/corefx@24889e2
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

IPv6 addresses not correctly formatted in Host headers
6 participants