Skip to content

Commit

Permalink
Merge pull request stratisproject#45 from quantumagi/master
Browse files Browse the repository at this point in the history
Remain up to date with Full Node Activation Branch
  • Loading branch information
Aprogiena authored Aug 24, 2018
2 parents 338eb2d + e052ddb commit d268311
Show file tree
Hide file tree
Showing 81 changed files with 1,589 additions and 506 deletions.
2 changes: 0 additions & 2 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ cache:
- '%USERPROFILE%\.nuget\packages'

init:
# install .NET Core SDK
- ps: choco install dotnetcore-sdk --no-progress --confirm --version 2.1.301
- ps: dotnet --info

#---------------------------------#
Expand Down
60 changes: 59 additions & 1 deletion src/Stratis.Bitcoin.Api.Tests/ApiSettingsTest.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using FluentAssertions;
using NBitcoin;
using Stratis.Bitcoin.Builder;
using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Features.Api;
using Stratis.Bitcoin.Networks;
using Stratis.Bitcoin.Tests.Common;
using Xunit;

Expand Down Expand Up @@ -60,6 +60,9 @@ public void GivenNoApiSettingsAreProvided_AndOnStratisNetwork_ThenDefaultSetting
// Assert.
Assert.Equal(ApiSettings.DefaultStratisApiPort, settings.ApiPort);
Assert.Equal(new Uri($"{ApiSettings.DefaultApiHost}:{ApiSettings.DefaultStratisApiPort}"), settings.ApiUri);

settings.HttpsCertificateFilePath.Should().BeNull();
settings.UseHttps.Should().BeFalse();
}

/// <summary>
Expand Down Expand Up @@ -257,5 +260,60 @@ public void GivenStratisTestnet_ThenUseTheCorrectPort()
// Assert.
Assert.Equal(ApiSettings.TestStratisApiPort, settings.ApiPort);
}

[Theory]
[InlineData(true, @"https://")]
[InlineData(false, @"http://")]
public void GivenUseHttps_ThenUsesTheCorrectProtocol(bool useHttps, string expectedProtocolPrefix)
{
// Arrange.
var nodeSettings = new NodeSettings(KnownNetworks.TestNet, args: new[] { $"-usehttps={useHttps}", "-certificatefilepath=nonNullValue" });

// Act.
var settings = new FullNodeBuilder()
.UseNodeSettings(nodeSettings)
.UseApi()
.Build()
.NodeService<ApiSettings>();

// Assert.
settings.UseHttps.Should().Be(useHttps);
settings.ApiUri.ToString().Should().StartWith(expectedProtocolPrefix);
}

[Fact]
public void GivenCertificateFilePath_ThenUsesTheCorrectFileName()
{
// Arrange.
var certificateFileName = @"abcd/someCertificate.pfx";
var nodeSettings = new NodeSettings(KnownNetworks.TestNet, args: new[] { $"-certificatefilepath={certificateFileName}" });

// Act.
var settings = new FullNodeBuilder()
.UseNodeSettings(nodeSettings)
.UseApi()
.Build()
.NodeService<ApiSettings>();

// Assert.
settings.HttpsCertificateFilePath.Should().Be(certificateFileName);
}

[Fact]
public void GivenUseHttpsAndNoCertificateFilePath_ThenShouldThrowConfigurationException()
{
// Arrange.
var nodeSettings = new NodeSettings(KnownNetworks.TestNet, args: new[] { $"-usehttps={true}" });

// Act.
var settingsAction = new Action(() =>
{
new FullNodeBuilder().UseNodeSettings(nodeSettings).UseApi().Build()
.NodeService<ApiSettings>();
});

// Assert.
settingsAction.Should().Throw<ConfigurationException>();
}
}
}
60 changes: 60 additions & 0 deletions src/Stratis.Bitcoin.Api.Tests/ProgramTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Security.Cryptography.X509Certificates;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using NSubstitute;
using Stratis.Bitcoin.Features.Api;
using Xunit;

namespace Stratis.Bitcoin.Api.Tests
{
public class ProgramTest
{
private readonly X509Certificate2 certificateToUse;
private readonly ICertificateStore certificateStore;
private readonly ApiSettings apiSettings;
private readonly IWebHostBuilder webHostBuilder;

private X509Certificate2 certificateRetrieved;

public ProgramTest()
{
this.apiSettings = new ApiSettings { UseHttps = true };
this.certificateToUse = new X509Certificate2();
this.certificateStore = Substitute.For<ICertificateStore>();
this.webHostBuilder = Substitute.For<IWebHostBuilder>();
}

[Fact]
public void Initialize_WhenCertificateRetrieved_UsesCertificateOnHttpsWithKestrel()
{
this.apiSettings.UseHttps = true;
this.SetCertificateInStore(true);

this.certificateRetrieved.Should().BeNull();

Program.Initialize(null, new FullNode(), this.apiSettings, this.certificateStore, this.webHostBuilder);

this.certificateRetrieved.Should().NotBeNull();
this.certificateRetrieved.Should().Be(this.certificateToUse);
this.certificateStore.ReceivedWithAnyArgs(1).TryGet(null, out _);
}

[Fact]
public void Initialize_WhenNotUsing_Https_ShouldNotLookForCertificates()
{
this.apiSettings.UseHttps = false;
this.SetCertificateInStore(true);

Program.Initialize(null, new FullNode(), this.apiSettings, this.certificateStore, this.webHostBuilder);

this.certificateStore.DidNotReceiveWithAnyArgs().TryGet(null, out _);
}

private void SetCertificateInStore(bool isCertInStore)
{
this.certificateStore.TryGet(this.apiSettings.HttpsCertificateFilePath, out this.certificateRetrieved)
.Returns(isCertInStore)
.AndDoes(_ => { this.certificateRetrieved = this.certificateToUse; });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="NSubstitute" Version="3.1.0" />
</ItemGroup>

<ItemGroup>
Expand Down
29 changes: 17 additions & 12 deletions src/Stratis.Bitcoin.Features.Api/ApiFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,44 @@ public sealed class ApiFeature : FullNodeFeature

private IWebHost webHost;

private readonly ICertificateStore certificateStore;

public ApiFeature(
IFullNodeBuilder fullNodeBuilder,
FullNode fullNode,
ApiFeatureOptions apiFeatureOptions,
ApiSettings apiSettings,
ILoggerFactory loggerFactory)
ILoggerFactory loggerFactory,
ICertificateStore certificateStore)
{
this.fullNodeBuilder = fullNodeBuilder;
this.fullNode = fullNode;
this.apiFeatureOptions = apiFeatureOptions;
this.apiSettings = apiSettings;
this.certificateStore = certificateStore;
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
}

public override void Initialize()
{
this.logger.LogInformation("API starting on URL '{0}'.", this.apiSettings.ApiUri);
this.webHost = Program.Initialize(this.fullNodeBuilder.Services, this.fullNode, this.apiSettings);
this.webHost = Program.Initialize(this.fullNodeBuilder.Services, this.fullNode, this.apiSettings, this.certificateStore, new WebHostBuilder());

if (this.apiSettings.KeepaliveTimer == null)
return;

// Start the keepalive timer, if set.
// If the timer expires, the node will shut down.
if (this.apiSettings.KeepaliveTimer != null)
this.apiSettings.KeepaliveTimer.Elapsed += (sender, args) =>
{
this.apiSettings.KeepaliveTimer.Elapsed += (sender, args) =>
{
this.logger.LogInformation($"The application will shut down because the keepalive timer has elapsed.");
this.logger.LogInformation($"The application will shut down because the keepalive timer has elapsed.");

this.apiSettings.KeepaliveTimer.Stop();
this.apiSettings.KeepaliveTimer.Enabled = false;
this.fullNode.NodeLifetime.StopApplication();
};
this.apiSettings.KeepaliveTimer.Stop();
this.apiSettings.KeepaliveTimer.Enabled = false;
this.fullNode.NodeLifetime.StopApplication();
};

this.apiSettings.KeepaliveTimer.Start();
}
this.apiSettings.KeepaliveTimer.Start();
}

/// <summary>
Expand Down Expand Up @@ -129,6 +133,7 @@ public static IFullNodeBuilder UseApi(this IFullNodeBuilder fullNodeBuilder, Act
services.AddSingleton(fullNodeBuilder);
services.AddSingleton(options);
services.AddSingleton<ApiSettings>();
services.AddSingleton<ICertificateStore, CertificateStore>();
});
});

Expand Down
43 changes: 36 additions & 7 deletions src/Stratis.Bitcoin.Features.Api/ApiSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ public class ApiSettings
/// <summary>URI to node's API interface.</summary>
public Timer KeepaliveTimer { get; private set; }

/// <summary>
/// The HTTPS certificate file path.
/// </summary>
/// <remarks>
/// Password protected certificates are not supported. On MacOs, only p12 certificates can be used without password.
/// Please refer to .Net Core documentation for usage: <seealso cref="https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2.-ctor?view=netcore-2.1#System_Security_Cryptography_X509Certificates_X509Certificate2__ctor_System_Byte___" />.
/// </remarks>
public string HttpsCertificateFilePath { get; set; }

/// <summary>Use HTTPS or not.</summary>
public bool UseHttps { get; set; }

/// <summary>
/// Initializes an instance of the object from the default configuration.
/// </summary>
Expand All @@ -60,7 +72,17 @@ public ApiSettings(NodeSettings nodeSettings)

TextFileConfiguration config = nodeSettings.ConfigReader;

string apiHost = config.GetOrDefault("apiuri", DefaultApiHost, this.logger);
this.UseHttps = config.GetOrDefault("usehttps", false);
this.HttpsCertificateFilePath = config.GetOrDefault("certificatefilepath", (string)null);

if (this.UseHttps && string.IsNullOrWhiteSpace(this.HttpsCertificateFilePath))
throw new ConfigurationException("The path to a certificate needs to be provided when using https. Please use the argument 'certificatefilepath' to provide it.");

var defaultApiHost = this.UseHttps
? DefaultApiHost.Replace(@"http://", @"https://")
: DefaultApiHost;

string apiHost = config.GetOrDefault("apiuri", defaultApiHost, this.logger);
var apiUri = new Uri(apiHost);

// Find out which port should be used for the API.
Expand Down Expand Up @@ -112,9 +134,11 @@ public static void PrintHelp(Network network)
{
var builder = new StringBuilder();

builder.AppendLine($"-apiuri=<string> URI to node's API interface. Defaults to '{ DefaultApiHost }'.");
builder.AppendLine($"-apiport=<0-65535> Port of node's API interface. Defaults to { GetDefaultPort(network) }.");
builder.AppendLine($"-keepalive=<seconds> Keep Alive interval (set in seconds). Default: 0 (no keep alive).");
builder.AppendLine($"-apiuri=<string> URI to node's API interface. Defaults to '{ DefaultApiHost }'.");
builder.AppendLine($"-apiport=<0-65535> Port of node's API interface. Defaults to { GetDefaultPort(network) }.");
builder.AppendLine($"-keepalive=<seconds> Keep Alive interval (set in seconds). Default: 0 (no keep alive).");
builder.AppendLine($"-usehttps=<bool> Use https protocol on the API. Defaults to false.");
builder.AppendLine($"-certificatefilepath=<string> Path to the certificate used for https traffic encryption. Defaults to <null>. Password protected files are not supported. On MacOs, only p12 certificates can be used without password.");

NodeSettings.Default().Logger.LogInformation(builder.ToString());
}
Expand All @@ -127,12 +151,17 @@ public static void PrintHelp(Network network)
public static void BuildDefaultConfigurationFile(StringBuilder builder, Network network)
{
builder.AppendLine("####API Settings####");
builder.AppendLine($"#URI to node's API interface. Defaults to '{ DefaultApiHost }'");
builder.AppendLine($"#URI to node's API interface. Defaults to '{ DefaultApiHost }'.");
builder.AppendLine($"#apiuri={ DefaultApiHost }");
builder.AppendLine($"#Port of node's API interface. Defaults to { GetDefaultPort(network) }");
builder.AppendLine($"#Port of node's API interface. Defaults to { GetDefaultPort(network) }.");
builder.AppendLine($"#apiport={ GetDefaultPort(network) }");
builder.AppendLine($"#Keep Alive interval (set in seconds). Default: 0 (no keep alive)");
builder.AppendLine($"#Keep Alive interval (set in seconds). Default: 0 (no keep alive).");
builder.AppendLine($"#keepalive=0");
builder.AppendLine($"#Use HTTPS protocol on the API. Default is false.");
builder.AppendLine($"#usehttps=false");
builder.AppendLine($"#Path to the file containing the certificate to use for https traffic encryption. Password protected files are not supported. On MacOs, only p12 certificates can be used without password.");
builder.AppendLine(@"#Please refer to .Net Core documentation for usage: 'https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2.-ctor?view=netcore-2.1#System_Security_Cryptography_X509Certificates_X509Certificate2__ctor_System_Byte___'.");
builder.AppendLine($"#certificatefilepath=");
}
}
}
34 changes: 34 additions & 0 deletions src/Stratis.Bitcoin.Features.Api/CertificateStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;

namespace Stratis.Bitcoin.Features.Api
{
public class CertificateStore : ICertificateStore
{
private readonly ILogger logger;

public CertificateStore(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
}

/// <inheritdoc />
public bool TryGet(string filePath, out X509Certificate2 certificate)
{
try
{
var fileInBytes = File.ReadAllBytes(filePath);
certificate = new X509Certificate2(fileInBytes);
return true;
}
catch (Exception e)
{
this.logger.LogWarning("Failed to read certificate at {0} : {1}", filePath, e.Message);
certificate = null;
return false;
}
}
}
}
18 changes: 18 additions & 0 deletions src/Stratis.Bitcoin.Features.Api/ICertificateStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Security.Cryptography.X509Certificates;

namespace Stratis.Bitcoin.Features.Api
{
/// <summary>
/// An interface providing operations on certificate repositories.
/// </summary>
public interface ICertificateStore
{
/// <summary>
/// Tries to retrieve a certificate from the file system.
/// </summary>
/// <param name="filePath">The full path of the certificate file.</param>
/// <param name="certificate">The certificate, if found.</param>
/// <returns>A value indicating whether or not the certificate has been found at the specified location.</returns>
bool TryGet(string filePath, out X509Certificate2 certificate);
}
}
Loading

0 comments on commit d268311

Please sign in to comment.