Can configure BTCPay SSH connection at startup

This commit is contained in:
nicolas.dorier
2018-08-12 21:38:45 +09:00
parent 29513d4ded
commit 6a1eca760a
13 changed files with 318 additions and 29 deletions

View File

@@ -116,6 +116,9 @@
</ItemGroup>
<ItemGroup>
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LNDGRPCServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>

View File

@@ -11,6 +11,7 @@ using StandardConfiguration;
using Microsoft.Extensions.Configuration;
using NBXplorer;
using BTCPayServer.Payments.Lightning;
using Renci.SshNet;
namespace BTCPayServer.Configuration
{
@@ -21,6 +22,69 @@ namespace BTCPayServer.Configuration
public string CookieFile { get; internal set; }
}
public class SSHSettings
{
public string Server { get; set; }
public int Port { get; set; } = 22;
public string KeyFile { get; set; }
public string KeyFilePassword { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public ConnectionInfo CreateConnectionInfo()
{
if (!string.IsNullOrEmpty(KeyFile))
{
return new ConnectionInfo(Server, Port, Username, new[] { new PrivateKeyAuthenticationMethod(Username, new PrivateKeyFile(KeyFile, KeyFilePassword)) });
}
else
{
return new ConnectionInfo(Server, Port, Username, new[] { new PasswordAuthenticationMethod(Username, Password) });
}
}
public static SSHSettings ParseConfiguration(IConfiguration conf)
{
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
{
var parts = settings.Server.Split(':');
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
{
settings.Port = port;
settings.Server = parts[0];
}
else
{
settings.Port = 22;
}
parts = settings.Server.Split('@');
if (parts.Length == 2)
{
settings.Username = parts[0];
settings.Server = parts[1];
}
else
{
settings.Username = "root";
}
}
else if(externalUrl != null)
{
settings.Port = 22;
settings.Username = "root";
settings.Server = externalUrl.DnsSafeHost;
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
return settings;
}
}
public class BTCPayServerOptions
{
public NetworkType NetworkType
@@ -117,6 +181,28 @@ namespace BTCPayServer.Configuration
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var sshSettings = SSHSettings.ParseConfiguration(conf);
if (!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile))
{
if (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
throw new ConfigException($"sshkeyfile does not exist");
if (sshSettings.Port > ushort.MaxValue ||
sshSettings.Port < ushort.MinValue)
throw new ConfigException($"ssh port is invalid");
if (!string.IsNullOrEmpty(sshSettings.Password) && !string.IsNullOrEmpty(sshSettings.KeyFile))
throw new ConfigException($"sshpassword or sshkeyfile should be provided, but not both");
try
{
sshSettings.CreateConnectionInfo();
}
catch
{
throw new ConfigException($"sshkeyfilepassword is invalid");
}
SSHSettings = sshSettings;
}
RootPath = conf.GetOrDefault<string>("rootpath", "/");
if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase))
RootPath = "/" + RootPath;
@@ -144,6 +230,11 @@ namespace BTCPayServer.Configuration
get;
set;
}
public SSHSettings SSHSettings
{
get;
set;
}
internal string GetRootUri()
{

View File

@@ -34,6 +34,10 @@ namespace BTCPayServer.Configuration
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();

View File

@@ -160,6 +160,7 @@ namespace BTCPayServer.Controllers
MaintenanceViewModel vm = new MaintenanceViewModel();
vm.UserName = "btcpayserver";
vm.DNSDomain = this.Request.Host.Host;
vm.SetConfiguredSSH(_Options.SSHSettings);
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
vm.DNSDomain = null;
return View(vm);
@@ -170,6 +171,7 @@ namespace BTCPayServer.Controllers
{
if (!ModelState.IsValid)
return View(vm);
vm.SetConfiguredSSH(_Options.SSHSettings);
if (command == "changedomain")
{
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
@@ -256,7 +258,8 @@ namespace BTCPayServer.Controllers
private IActionResult RunSSH(MaintenanceViewModel vm, string ssh)
{
ssh = $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && nohup {ssh} > /dev/null 2>&1 & disown'";
var sshClient = vm.CreateSSHClient(this.Request.Host.Host);
var sshClient = _Options.SSHSettings == null ? vm.CreateSSHClient(this.Request.Host.Host)
: new SshClient(_Options.SSHSettings.CreateConnectionInfo());
try
{
sshClient.Connect();
@@ -404,13 +407,14 @@ namespace BTCPayServer.Controllers
}
}
}
result.HasSSH = _Options.SSHSettings != null;
return View(result);
}
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
public IActionResult LNDGRPCServices(string cryptoCode, int index, uint? nonce)
{
if(!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
@@ -507,6 +511,27 @@ namespace BTCPayServer.Controllers
return connectionString;
}
[Route("server/services/ssh")]
public IActionResult SSHService(bool downloadKeyFile = false)
{
var settings = _Options.SSHSettings;
if (settings == null)
return NotFound();
if (downloadKeyFile)
{
if (!System.IO.File.Exists(settings.KeyFile))
return NotFound();
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
}
SSHServiceViewModel vm = new SSHServiceViewModel();
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
vm.Password = settings.Password;
vm.KeyFilePassword = settings.KeyFilePassword;
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
return View(vm);
}
[Route("server/theme")]
public async Task<IActionResult> Theme()
{

View File

@@ -0,0 +1,67 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services;
using Microsoft.Extensions.Hosting;
using System.Threading;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
namespace BTCPayServer.HostedServices
{
public class CheckConfigurationHostedService : IHostedService
{
private readonly BTCPayServerOptions _options;
public CheckConfigurationHostedService(BTCPayServerOptions options)
{
_options = options;
}
public Task StartAsync(CancellationToken cancellationToken)
{
new Thread(() =>
{
if (_options.SSHSettings != null)
{
Logs.Configuration.LogInformation($"SSH settings detected, testing connection to {_options.SSHSettings.Username}@{_options.SSHSettings.Server} on port {_options.SSHSettings.Port} ...");
var connection = new Renci.SshNet.SshClient(_options.SSHSettings.CreateConnectionInfo());
try
{
connection.Connect();
connection.Disconnect();
Logs.Configuration.LogInformation($"SSH connection succeeded");
}
catch (Renci.SshNet.Common.SshAuthenticationException)
{
Logs.Configuration.LogWarning($"SSH invalid credentials");
}
catch (Exception ex)
{
var message = ex.Message;
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
{
message = aggrEx.InnerException.Message;
}
Logs.Configuration.LogWarning($"SSH connection issue: {message}");
}
finally
{
connection.Dispose();
}
}
})
{ IsBackground = true }.Start();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@@ -118,6 +118,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();

View File

@@ -3,12 +3,14 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using Renci.SshNet;
namespace BTCPayServer.Models.ServerViewModels
{
public class MaintenanceViewModel
{
public bool ExposedSSH { get; set; }
[Required]
public string UserName { get; set; }
[Required]
@@ -20,5 +22,15 @@ namespace BTCPayServer.Models.ServerViewModels
{
return new SshClient(host, UserName, Password);
}
internal void SetConfiguredSSH(SSHSettings settings)
{
if(settings != null)
{
ExposedSSH = true;
UserName = "unknown";
Password = "unknown";
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class SSHServiceViewModel
{
public string CommandLine { get; set; }
public string Password { get; set; }
public string KeyFilePassword { get; set; }
public bool HasKeyFile { get; set; }
}
}

View File

@@ -14,5 +14,6 @@ namespace BTCPayServer.Models.ServerViewModels
public int Index { get; set; }
}
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
public bool HasSSH { get; set; }
}
}

View File

@@ -5,14 +5,14 @@
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_BUNDLEJSCSS": "false"
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
},
"applicationUrl": "http://127.0.0.1:14142/"
}

View File

@@ -11,6 +11,8 @@
<div class="col-md-8">
<form method="post">
@if(!Model.ExposedSSH)
{
<div class="form-group">
<h5>SSH Settings</h5>
<span>For changing any settings, you need to enter your SSH credentials:</span>
@@ -26,7 +28,14 @@
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
}
else
{
<input asp-for="Password" type="hidden" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
<input asp-for="UserName" type="hidden" class="form-control" />
<span asp-validation-for="UserName" class="text-danger"></span>
}
<div class="form-group">
<h5>Change domain name</h5>
<span>You can change the domain name of your server by following <a href="https://github.com/btcpayserver/btcpayserver-doc/blob/master/ChangeDomain.md">this guide</a></span>

View File

@@ -0,0 +1,50 @@
@model BTCPayServer.Models.ServerViewModels.SSHServiceViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>SSH settings</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>
<span>SSH servies are used by the maintenance operations<br /></span>
</p>
</div>
<div class="form-group">
<div class="form-group">
<label asp-for="CommandLine"></label>
<input asp-for="CommandLine" readonly class="form-control" />
</div>
@if(!string.IsNullOrEmpty(Model.Password))
{
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" readonly class="form-control" />
</div>
}
@if(!string.IsNullOrEmpty(Model.KeyFilePassword))
{
<div class="form-group">
<label asp-for="KeyFilePassword"></label>
<input asp-for="KeyFilePassword" readonly class="form-control" />
</div>
}
@if(Model.HasKeyFile)
{
<a class="btn btn-primary form-control" asp-action="SSHService" asp-route-downloadKeyFile="true">Download Key File</a>
}
</div>
</div>
</div>

View File

@@ -41,6 +41,16 @@
</td>
</tr>
}
@if(Model.HasSSH)
{
<tr>
<td>None</td>
<td>SSH</td>
<td style="text-align:right">
<a asp-action="SSHService">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>