Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow browser headers to be set for the friendly html page #43

Merged
merged 1 commit into from
Nov 2, 2017
Merged
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 @@ -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<KeyValuePair<string, string>> headers) { }
public void Trust(string clientThumbprint) { }
public void TrustOnly(IReadOnlyList<string> thumbprints) { }
}
Expand Down Expand Up @@ -348,7 +349,9 @@ public void NotifyUsed() { }
public class SecureListener : IDisposable
{
public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action<Halibut.Transport.Protocol.MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent) { }
public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action<Halibut.Transport.Protocol.MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders) { }
public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func<Halibut.Transport.Protocol.MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent) { }
public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func<Halibut.Transport.Protocol.MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders) { }
public void Dispose() { }
public int Start() { }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyValuePair<string, string>> headers) { }
public void Trust(string clientThumbprint) { }
public void TrustOnly(IReadOnlyList<string> thumbprints) { }
}
Expand Down Expand Up @@ -344,7 +345,9 @@ public void NotifyUsed() { }
public class SecureListener : IDisposable
{
public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action<Halibut.Transport.Protocol.MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent) { }
public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action<Halibut.Transport.Protocol.MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders) { }
public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func<Halibut.Transport.Protocol.MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent) { }
public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func<Halibut.Transport.Protocol.MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders) { }
public void Dispose() { }
public int Start() { }
}
Expand All @@ -358,7 +361,9 @@ public void ExecuteTransaction(Action<Halibut.Transport.Protocol.MessageExchange
public class SecureWebSocketListener : IDisposable
{
public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Action<Halibut.Transport.Protocol.MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent) { }
public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Action<Halibut.Transport.Protocol.MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders) { }
public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Func<Halibut.Transport.Protocol.MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent) { }
public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Func<Halibut.Transport.Protocol.MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, Halibut.Diagnostics.ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders) { }
public void Dispose() { }
public void Start() { }
}
Expand Down
48 changes: 48 additions & 0 deletions source/Halibut.Tests/UsageFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> {{"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()
Expand Down Expand Up @@ -315,6 +330,39 @@ static string DownloadStringIgnoringCertificateValidation(string uri)
#endif
}

static IEnumerable<KeyValuePair<string, string>> GetHeadersIgnoringCertificateValidation(string uri)
{
#if NET40
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just use HttpClient for both cases? No need to deal with ServicePointManager static shenanigans... :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would make sense...

Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately that is not that case with the latest versions, OctopusDeploy/OctopusClients#121

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<string, string>(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<string, string>(x.Key, string.Join(";", x.Value)));
}
#endif
}

#if HAS_SERVICE_POINT_MANAGER
static void AddSslCertToLocalStoreAndRegisterFor(string address)
{
Expand Down
12 changes: 9 additions & 3 deletions source/Halibut/HalibutRuntime.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,7 +16,6 @@ namespace Halibut
public class HalibutRuntime : IHalibutRuntime
{
public static readonly string DefaultFriendlyHtmlPageContent = "<html><body><p>Hello!</p></body></html>";

readonly ConcurrentDictionary<Uri, PendingRequestQueue> queues = new ConcurrentDictionary<Uri, PendingRequestQueue>();
readonly X509Certificate2 serverCertificate;
readonly List<IDisposable> listeners = new List<IDisposable>();
Expand All @@ -26,6 +26,7 @@ public class HalibutRuntime : IHalibutRuntime
readonly ConnectionPool<ServiceEndPoint, IConnection> pool = new ConnectionPool<ServiceEndPoint, IConnection>();
readonly PollingClientCollection pollingClients = new PollingClientCollection();
string friendlyHtmlPageContent = DefaultFriendlyHtmlPageContent;
Dictionary<string, string> friendlyHtmlPageHeaders = new Dictionary<string, string>();

public HalibutRuntime(X509Certificate2 serverCertificate) : this(new NullServiceFactory(), serverCertificate)
{
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -201,6 +202,11 @@ public void SetFriendlyHtmlPageContent(string html)
friendlyHtmlPageContent = html ?? DefaultFriendlyHtmlPageContent;
}

public void SetFriendlyHtmlPageHeaders(IEnumerable<KeyValuePair<string, string>> headers)
{
friendlyHtmlPageHeaders = headers?.ToDictionary(x => x.Key, x => x.Value) ?? new Dictionary<string, string>();
}

public void Dispose()
{
pollingClients.Dispose();
Expand Down
19 changes: 18 additions & 1 deletion source/Halibut/Transport/SecureListener.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
Expand Down Expand Up @@ -34,24 +35,37 @@ public class SecureListener : IDisposable
readonly Predicate<string> verifyClientThumbprint;
readonly ILogFactory logFactory;
readonly Func<string> getFriendlyHtmlPageContent;
readonly Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders;
readonly CancellationTokenSource cts = new CancellationTokenSource();
ILog log;
TcpListener listener;

public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action<MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, ILogFactory logFactory, Func<string> 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<string, string>())

{
}

public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Action<MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders)
: this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, getFriendlyHtmlPageHeaders)

{
}

public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func<MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent)
: this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, () => new Dictionary<string, string>())
{
}

public SecureListener(IPEndPoint endPoint, X509Certificate2 serverCertificate, Func<MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders)
{
this.endPoint = endPoint;
this.serverCertificate = serverCertificate;
this.protocolHandler = protocolHandler;
this.verifyClientThumbprint = verifyClientThumbprint;
this.logFactory = logFactory;
this.getFriendlyHtmlPageContent = getFriendlyHtmlPageContent;
this.getFriendlyHtmlPageHeaders = getFriendlyHtmlPageHeaders;
EnsureCertificateIsValidForListening(serverCertificate);
}

Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down
20 changes: 19 additions & 1 deletion source/Halibut/Transport/SecureWebSocketListener.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,17 +21,29 @@ public class SecureWebSocketListener : IDisposable
readonly Predicate<string> verifyClientThumbprint;
readonly ILogFactory logFactory;
readonly Func<string> getFriendlyHtmlPageContent;
readonly Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders;
readonly CancellationTokenSource cts = new CancellationTokenSource();
ILog log;
HttpListener listener;

public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Action<MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, ILogFactory logFactory, Func<string> 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<string, string>())

{
}

public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Action<MessageExchangeProtocol> protocolHandler, Predicate<string> verifyClientThumbprint, ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders)
: this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, getFriendlyHtmlPageHeaders)

{
}

public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Func<MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent)
: this(endPoint, serverCertificate, h => Task.Run(() => protocolHandler(h)), verifyClientThumbprint, logFactory, getFriendlyHtmlPageContent, () => new Dictionary<string, string>())
{
}

public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertificate, Func<MessageExchangeProtocol, Task> protocolHandler, Predicate<string> verifyClientThumbprint, ILogFactory logFactory, Func<string> getFriendlyHtmlPageContent, Func<Dictionary<string, string>> getFriendlyHtmlPageHeaders)
{
if (!endPoint.EndsWith("/"))
endPoint += "/";
Expand All @@ -41,6 +54,7 @@ public SecureWebSocketListener(string endPoint, X509Certificate2 serverCertifica
this.verifyClientThumbprint = verifyClientThumbprint;
this.logFactory = logFactory;
this.getFriendlyHtmlPageContent = getFriendlyHtmlPageContent;
this.getFriendlyHtmlPageHeaders = getFriendlyHtmlPageHeaders;
EnsureCertificateIsValidForListening(serverCertificate);
}

Expand Down Expand Up @@ -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)))
Expand Down