From dac3da8b41d53f78258e7d7043d023f088179c1f Mon Sep 17 00:00:00 2001 From: Matt Richardson Date: Fri, 27 Oct 2017 15:40:26 +1100 Subject: [PATCH] Allow browser headers to be set for the friendly html page Allows us to set security related headers such as `X-Frame-Options` to avoid flagging up on security scanning tools Related to https://github.com/OctopusDeploy/Issues/issues/3884 --- ...ceAreaShouldNotRegress.NETCore.approved.cs | 3 ++ ...aShouldNotRegress.NETFramework.approved.cs | 5 ++ source/Halibut.Tests/UsageFixture.cs | 48 +++++++++++++++++++ source/Halibut/HalibutRuntime.cs | 12 +++-- source/Halibut/Transport/SecureListener.cs | 19 +++++++- .../Transport/SecureWebSocketListener.cs | 20 +++++++- 6 files changed, 102 insertions(+), 5 deletions(-) diff --git a/source/Halibut.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress.NETCore.approved.cs b/source/Halibut.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress.NETCore.approved.cs index 38ffc169..071653eb 100644 --- a/source/Halibut.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress.NETCore.approved.cs +++ b/source/Halibut.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress.NETCore.approved.cs @@ -55,6 +55,7 @@ public void Poll(Uri subscription, Halibut.ServiceEndPoint endPoint) { } public void RemoveTrust(string clientThumbprint) { } public void Route(Halibut.ServiceEndPoint to, Halibut.ServiceEndPoint via) { } public void SetFriendlyHtmlPageContent(string html) { } + public void SetFriendlyHtmlPageHeaders(IEnumerable> headers) { } public void Trust(string clientThumbprint) { } public void TrustOnly(IReadOnlyList thumbprints) { } } @@ -348,7 +349,9 @@ public void NotifyUsed() { } public class SecureListener : IDisposable { public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent) { } + public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) { } public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent) { } + public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) { } public void Dispose() { } public int Start() { } } diff --git a/source/Halibut.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress.NETFramework.approved.cs b/source/Halibut.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress.NETFramework.approved.cs index 90fd87f7..50207bab 100644 --- a/source/Halibut.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress.NETFramework.approved.cs +++ b/source/Halibut.Tests/PublicSurfaceAreaFixture.ThePublicSurfaceAreaShouldNotRegress.NETFramework.approved.cs @@ -56,6 +56,7 @@ public void Poll(Uri subscription, Halibut.ServiceEndPoint endPoint) { } public void RemoveTrust(string clientThumbprint) { } public void Route(Halibut.ServiceEndPoint to, Halibut.ServiceEndPoint via) { } public void SetFriendlyHtmlPageContent(string html) { } + public void SetFriendlyHtmlPageHeaders(IEnumerable> headers) { } public void Trust(string clientThumbprint) { } public void TrustOnly(IReadOnlyList thumbprints) { } } @@ -344,7 +345,9 @@ public void NotifyUsed() { } public class SecureListener : IDisposable { public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent) { } + public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) { } public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent) { } + public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) { } public void Dispose() { } public int Start() { } } @@ -358,7 +361,9 @@ public void ExecuteTransaction(Action protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent) { } + public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) { } public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent) { } + public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) { } public void Dispose() { } public void Start() { } } diff --git a/source/Halibut.Tests/UsageFixture.cs b/source/Halibut.Tests/UsageFixture.cs index 11ba20bc..708d709f 100644 --- a/source/Halibut.Tests/UsageFixture.cs +++ b/source/Halibut.Tests/UsageFixture.cs @@ -262,6 +262,21 @@ public void CanSetCustomFriendlyHtmlPage(string html, string expectedResult = nu } } + [Fact] + public void CanSetCustomFriendlyHtmlPageHeaders() + { + using (var octopus = new HalibutRuntime(services, Certificates.Octopus)) + { + octopus.SetFriendlyHtmlPageHeaders(new Dictionary {{"X-Content-Type-Options", "nosniff"}, {"X-Frame-Options", "DENY"}}); + var listenPort = octopus.Listen(); + + var result = GetHeadersIgnoringCertificateValidation("https://localhost:" + listenPort).ToList(); + + result.Should().Contain(x => x.Key == "X-Content-Type-Options" && x.Value == "nosniff"); + result.Should().Contain(x => x.Key == "X-Frame-Options" && x.Value == "DENY"); + } + } + [Fact] [Description("Connecting over a non-secure connection should cause the socket to be closed by the server. The socket used to be held open indefinitely for any failure to establish an SslStream.")] public void ConnectingOverHttpShouldFailQuickly() @@ -315,6 +330,39 @@ static string DownloadStringIgnoringCertificateValidation(string uri) #endif } + static IEnumerable> GetHeadersIgnoringCertificateValidation(string uri) + { +#if NET40 + using (var webClient = new WebClient()) + { + try + { + // We need to ignore server certificate validation errors - the server certificate is self-signed + ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true; + var response = webClient.DownloadString(uri); + foreach (string key in webClient.ResponseHeaders) + { + yield return new KeyValuePair(key, webClient.ResponseHeaders[key]); + } + } + finally + { + // And restore it back to default behaviour + ServicePointManager.ServerCertificateValidationCallback = null; + } + } +#else + var handler = new HttpClientHandler(); + // We need to ignore server certificate validation errors - the server certificate is self-signed + handler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, errors) => true; + using (var webClient = new HttpClient(handler)) + { + var response = webClient.GetAsync(uri).GetAwaiter().GetResult(); + return response.Headers.Select(x => new KeyValuePair(x.Key, string.Join(";", x.Value))); + } +#endif + } + #if HAS_SERVICE_POINT_MANAGER static void AddSslCertToLocalStoreAndRegisterFor(string address) { diff --git a/source/Halibut/HalibutRuntime.cs b/source/Halibut/HalibutRuntime.cs index 169f982c..b8db5a9e 100644 --- a/source/Halibut/HalibutRuntime.cs +++ b/source/Halibut/HalibutRuntime.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; @@ -15,7 +16,6 @@ namespace Halibut public class HalibutRuntime : IHalibutRuntime { public static readonly string DefaultFriendlyHtmlPageContent = "

Hello!

"; - readonly ConcurrentDictionary queues = new ConcurrentDictionary(); readonly X509Certificate2 serverCertificate; readonly List listeners = new List(); @@ -26,6 +26,7 @@ public class HalibutRuntime : IHalibutRuntime readonly ConnectionPool pool = new ConnectionPool(); readonly PollingClientCollection pollingClients = new PollingClientCollection(); string friendlyHtmlPageContent = DefaultFriendlyHtmlPageContent; + Dictionary friendlyHtmlPageHeaders = new Dictionary(); public HalibutRuntime(X509Certificate2 serverCertificate) : this(new NullServiceFactory(), serverCertificate) { @@ -59,14 +60,14 @@ public int Listen(int port) public int Listen(IPEndPoint endpoint) { - var listener = new SecureListener(endpoint, serverCertificate, ListenerHandler, IsTrusted, logs, () => friendlyHtmlPageContent); + var listener = new SecureListener(endpoint, serverCertificate, ListenerHandler, IsTrusted, logs, () => friendlyHtmlPageContent, () => friendlyHtmlPageHeaders); listeners.Add(listener); return listener.Start(); } #if HAS_WEB_SOCKET_LISTENER public void ListenWebSocket(string endpoint) { - var listener = new SecureWebSocketListener(endpoint, serverCertificate, ListenerHandler, IsTrusted, logs, () => friendlyHtmlPageContent); + var listener = new SecureWebSocketListener(endpoint, serverCertificate, ListenerHandler, IsTrusted, logs, () => friendlyHtmlPageContent, () => friendlyHtmlPageHeaders); listeners.Add(listener); listener.Start(); } @@ -201,6 +202,11 @@ public void SetFriendlyHtmlPageContent(string html) friendlyHtmlPageContent = html ?? DefaultFriendlyHtmlPageContent; } + public void SetFriendlyHtmlPageHeaders(IEnumerable> headers) + { + friendlyHtmlPageHeaders = headers?.ToDictionary(x => x.Key, x => x.Value) ?? new Dictionary(); + } + public void Dispose() { pollingClients.Dispose(); diff --git a/source/Halibut/Transport/SecureListener.cs b/source/Halibut/Transport/SecureListener.cs index 84778f83..79745a71 100644 --- a/source/Halibut/Transport/SecureListener.cs +++ b/source/Halibut/Transport/SecureListener.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Security; @@ -34,17 +35,29 @@ public class SecureListener : IDisposable readonly Predicate verifyClientThumbprint; readonly ILogFactory logFactory; readonly Func getFriendlyHtmlPageContent; + readonly Func> getFriendlyHtmlPageHeaders; readonly CancellationTokenSource cts = new CancellationTokenSource(); ILog log; TcpListener listener; public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, ILogFactory logFactory, Func getFriendlyHtmlPageContent) - : this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent) + : this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, () => new Dictionary()) + + { + } + + public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) + : this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, getFriendlyHtmlPageHeaders) { } public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, ILogFactory logFactory, Func getFriendlyHtmlPageContent) + : this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, () => new Dictionary()) + { + } + + public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) { this.endPoint = endPoint; this.serverCertificate = serverCertificate; @@ -52,6 +65,7 @@ public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, F this.verifyClientThumbprint = verifyClientThumbprint; this.logFactory = logFactory; this.getFriendlyHtmlPageContent = getFriendlyHtmlPageContent; + this.getFriendlyHtmlPageHeaders = getFriendlyHtmlPageHeaders; EnsureCertificateIsValidForListening(serverCertificate); } @@ -192,6 +206,7 @@ async Task ExecuteRequest(TcpClient client) void SendFriendlyHtmlPage(Stream stream) { var message = getFriendlyHtmlPageContent(); + var headers = getFriendlyHtmlPageHeaders(); // This could fail if the client terminates the connection and we attempt to write to it // Disposing the StreamWriter will close the stream - it owns the stream @@ -200,6 +215,8 @@ void SendFriendlyHtmlPage(Stream stream) writer.WriteLine("HTTP/1.0 200 OK"); writer.WriteLine("Content-Type: text/html; charset=utf-8"); writer.WriteLine("Content-Length: " + message.Length); + foreach(var header in headers) + writer.WriteLine($"{header.Key}: {header.Value}"); writer.WriteLine(); writer.WriteLine(message); writer.WriteLine(); diff --git a/source/Halibut/Transport/SecureWebSocketListener.cs b/source/Halibut/Transport/SecureWebSocketListener.cs index 3712e04d..132eed04 100644 --- a/source/Halibut/Transport/SecureWebSocketListener.cs +++ b/source/Halibut/Transport/SecureWebSocketListener.cs @@ -1,5 +1,6 @@ #if HAS_SERVICE_POINT_MANAGER using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography.X509Certificates; @@ -20,17 +21,29 @@ public class SecureWebSocketListener : IDisposable readonly Predicate verifyClientThumbprint; readonly ILogFactory logFactory; readonly Func getFriendlyHtmlPageContent; + readonly Func> getFriendlyHtmlPageHeaders; readonly CancellationTokenSource cts = new CancellationTokenSource(); ILog log; HttpListener listener; public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, ILogFactory logFactory, Func getFriendlyHtmlPageContent) - : this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent) + : this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, () => new Dictionary()) + + { + } + + public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Action protocolHandler, Predicate verifyClientThumbprint, ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) + : this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, getFriendlyHtmlPageHeaders) { } public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, ILogFactory logFactory, Func getFriendlyHtmlPageContent) + : this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, () => new Dictionary()) + { + } + + public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Func protocolHandler, Predicate verifyClientThumbprint, ILogFactory logFactory, Func getFriendlyHtmlPageContent, Func> getFriendlyHtmlPageHeaders) { if (!endPoint.EndsWith("/")) endPoint += "/"; @@ -41,6 +54,7 @@ public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertifica this.verifyClientThumbprint = verifyClientThumbprint; this.logFactory = logFactory; this.getFriendlyHtmlPageContent = getFriendlyHtmlPageContent; + this.getFriendlyHtmlPageHeaders = getFriendlyHtmlPageHeaders; EnsureCertificateIsValidForListening(serverCertificate); } @@ -162,7 +176,11 @@ async Task ExecuteRequest(HttpListenerContext listenerContext) void SendFriendlyHtmlPage(HttpListenerResponse response) { var message = getFriendlyHtmlPageContent(); + var headers = getFriendlyHtmlPageHeaders(); response.AddHeader("Content-Type", "text/html; charset=utf-8"); + foreach(var header in headers) + response.AddHeader(header.Key, header.Value); + // This could fail if the client terminates the connection and we attempt to write to it // Disposing the StreamWriter will close the stream - it owns the stream using (var writer = new StreamWriter(response.OutputStream, new UTF8Encoding(false)))