From 150bb3faf413226d053777bbc72618fb284a6970 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Mon, 18 Dec 2017 18:40:11 -0800 Subject: [PATCH] Add BindingAddress to HttpAbstractions (#977) --- .../Internal/BindingAddress.cs | 155 ++++++++++++++++++ .../Internal/Constants.cs | 1 + .../Internal/BindingAddressTests.cs | 70 ++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Http/Internal/BindingAddress.cs create mode 100644 test/Microsoft.AspNetCore.Http.Tests/Internal/BindingAddressTests.cs diff --git a/src/Microsoft.AspNetCore.Http/Internal/BindingAddress.cs b/src/Microsoft.AspNetCore.Http/Internal/BindingAddress.cs new file mode 100644 index 0000000000..492fa23dbe --- /dev/null +++ b/src/Microsoft.AspNetCore.Http/Internal/BindingAddress.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public class BindingAddress + { + public string Host { get; private set; } + public string PathBase { get; private set; } + public int Port { get; internal set; } + public string Scheme { get; private set; } + + public bool IsUnixPipe + { + get + { + return Host.StartsWith(Constants.UnixPipeHostPrefix, StringComparison.Ordinal); + } + } + + public string UnixPipePath + { + get + { + if (!IsUnixPipe) + { + throw new InvalidOperationException("Binding address is not a unix pipe."); + } + + return Host.Substring(Constants.UnixPipeHostPrefix.Length - 1); + } + } + + public override string ToString() + { + if (IsUnixPipe) + { + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant(); + } + else + { + return Scheme.ToLowerInvariant() + "://" + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + PathBase.ToString(CultureInfo.InvariantCulture); + } + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public override bool Equals(object obj) + { + var other = obj as BindingAddress; + if (other == null) + { + return false; + } + return string.Equals(Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) + && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) + && Port == other.Port + && PathBase == other.PathBase; + } + + public static BindingAddress Parse(string address) + { + address = address ?? string.Empty; + + int schemeDelimiterStart = address.IndexOf("://", StringComparison.Ordinal); + if (schemeDelimiterStart < 0) + { + throw new FormatException($"Invalid url: '{address}'"); + } + int schemeDelimiterEnd = schemeDelimiterStart + "://".Length; + + var isUnixPipe = address.IndexOf(Constants.UnixPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd; + + int pathDelimiterStart; + int pathDelimiterEnd; + if (!isUnixPipe) + { + pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart; + } + else + { + pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + Constants.UnixPipeHostPrefix.Length, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart + ":".Length; + } + + if (pathDelimiterStart < 0) + { + pathDelimiterStart = pathDelimiterEnd = address.Length; + } + + var serverAddress = new BindingAddress(); + serverAddress.Scheme = address.Substring(0, schemeDelimiterStart); + + var hasSpecifiedPort = false; + if (!isUnixPipe) + { + int portDelimiterStart = address.LastIndexOf(":", pathDelimiterStart - 1, pathDelimiterStart - schemeDelimiterEnd, StringComparison.Ordinal); + if (portDelimiterStart >= 0) + { + int portDelimiterEnd = portDelimiterStart + ":".Length; + + string portString = address.Substring(portDelimiterEnd, pathDelimiterStart - portDelimiterEnd); + int portNumber; + if (int.TryParse(portString, NumberStyles.Integer, CultureInfo.InvariantCulture, out portNumber)) + { + hasSpecifiedPort = true; + serverAddress.Host = address.Substring(schemeDelimiterEnd, portDelimiterStart - schemeDelimiterEnd); + serverAddress.Port = portNumber; + } + } + + if (!hasSpecifiedPort) + { + if (string.Equals(serverAddress.Scheme, "http", StringComparison.OrdinalIgnoreCase)) + { + serverAddress.Port = 80; + } + else if (string.Equals(serverAddress.Scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + serverAddress.Port = 443; + } + } + } + + if (!hasSpecifiedPort) + { + serverAddress.Host = address.Substring(schemeDelimiterEnd, pathDelimiterStart - schemeDelimiterEnd); + } + + if (string.IsNullOrEmpty(serverAddress.Host)) + { + throw new FormatException($"Invalid url: '{address}'"); + } + + if (address[address.Length - 1] == '/') + { + serverAddress.PathBase = address.Substring(pathDelimiterEnd, address.Length - pathDelimiterEnd - 1); + } + else + { + serverAddress.PathBase = address.Substring(pathDelimiterEnd); + } + + return serverAddress; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http/Internal/Constants.cs b/src/Microsoft.AspNetCore.Http/Internal/Constants.cs index 0d52a692d3..280011b3e0 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/Constants.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/Constants.cs @@ -7,6 +7,7 @@ namespace Microsoft.AspNetCore.Http.Internal { internal const string Http = "http"; internal const string Https = "https"; + internal const string UnixPipeHostPrefix = "unix:/"; internal static class BuilderProperties { diff --git a/test/Microsoft.AspNetCore.Http.Tests/Internal/BindingAddressTests.cs b/test/Microsoft.AspNetCore.Http.Tests/Internal/BindingAddressTests.cs new file mode 100644 index 0000000000..3bca310e82 --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Tests/Internal/BindingAddressTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; +namespace Microsoft.AspNetCore.Http.Internal.Tests +{ + public class BindingAddressTests + { + [Theory] + [InlineData("")] + [InlineData("5000")] + [InlineData("//noscheme")] + public void FromUriThrowsForUrlsWithoutSchemeDelimiter(string url) + { + Assert.Throws(() => BindingAddress.Parse(url)); + } + + [Theory] + [InlineData("://")] + [InlineData("://:5000")] + [InlineData("http://")] + [InlineData("http://:5000")] + [InlineData("http:///")] + [InlineData("http:///:5000")] + [InlineData("http:////")] + [InlineData("http:////:5000")] + public void FromUriThrowsForUrlsWithoutHost(string url) + { + Assert.Throws(() => BindingAddress.Parse(url)); + } + + [Theory] + [InlineData("://emptyscheme", "", "emptyscheme", 0, "", "://emptyscheme:0")] + [InlineData("http://+", "http", "+", 80, "", "http://+:80")] + [InlineData("http://*", "http", "*", 80, "", "http://*:80")] + [InlineData("http://localhost", "http", "localhost", 80, "", "http://localhost:80")] + [InlineData("http://www.example.com", "http", "www.example.com", 80, "", "http://www.example.com:80")] + [InlineData("https://www.example.com", "https", "www.example.com", 443, "", "https://www.example.com:443")] + [InlineData("http://www.example.com/", "http", "www.example.com", 80, "", "http://www.example.com:80")] + [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", 80, "/foo?bar=baz", "http://www.example.com:80/foo?bar=baz")] + [InlineData("http://www.example.com:5000", "http", "www.example.com", 5000, "", null)] + [InlineData("https://www.example.com:5000", "https", "www.example.com", 5000, "", null)] + [InlineData("http://www.example.com:5000/", "http", "www.example.com", 5000, "", "http://www.example.com:5000")] + [InlineData("http://www.example.com:NOTAPORT", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] + [InlineData("https://www.example.com:NOTAPORT", "https", "www.example.com:NOTAPORT", 443, "", "https://www.example.com:notaport:443")] + [InlineData("http://www.example.com:NOTAPORT/", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] + [InlineData("http://foo:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "foo:", 80, "/tmp/kestrel-test.sock:5000/doesn't/matter", "http://foo::80/tmp/kestrel-test.sock:5000/doesn't/matter")] + [InlineData("http://unix:foo/tmp/kestrel-test.sock", "http", "unix:foo", 80, "/tmp/kestrel-test.sock", "http://unix:foo:80/tmp/kestrel-test.sock")] + [InlineData("http://unix:5000/tmp/kestrel-test.sock", "http", "unix", 5000, "/tmp/kestrel-test.sock", "http://unix:5000/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock", "http", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("https://unix:/tmp/kestrel-test.sock", "https", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("http://unix:/tmp/kestrel-test.sock:", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "unix:/tmp/kestrel-test.sock", 0, "5000/doesn't/matter", "http://unix:/tmp/kestrel-test.sock")] + public void UrlsAreParsedCorrectly(string url, string scheme, string host, int port, string pathBase, string toString) + { + var serverAddress = BindingAddress.Parse(url); + + Assert.Equal(scheme, serverAddress.Scheme); + Assert.Equal(host, serverAddress.Host); + Assert.Equal(port, serverAddress.Port); + Assert.Equal(pathBase, serverAddress.PathBase); + + Assert.Equal(toString ?? url, serverAddress.ToString()); + } + } +}