Add ability to change domain from server settings

This commit is contained in:
nicolas.dorier 2018-07-24 17:04:57 +09:00
parent b0d6216ffc
commit 27cea81cb2
7 changed files with 220 additions and 4 deletions

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework> <TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.62</Version> <Version>1.0.2.63</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn> <NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -48,6 +48,7 @@
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" /> <PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" /> <PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" /> <PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" /> <PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup> </ItemGroup>
@ -118,6 +119,9 @@
<Content Update="Views\Server\LNDGRPCServices.cshtml"> <Content Update="Views\Server\LNDGRPCServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>
</Content> </Content>
<Content Update="Views\Server\Maintenance.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Services.cshtml"> <Content Update="Views\Server\Services.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>
</Content> </Content>

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models; using BTCPayServer.Models;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {

View file

@ -22,6 +22,8 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Mail; using System.Net.Mail;
using System.Threading.Tasks; using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@ -149,6 +151,138 @@ namespace BTCPayServer.Controllers
return View(userVM); return View(userVM);
} }
[Route("server/maintenance")]
public IActionResult Maintenance()
{
MaintenanceViewModel vm = new MaintenanceViewModel();
vm.UserName = "btcpayserver";
vm.DNSDomain = this.Request.Host.Host;
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
vm.DNSDomain = null;
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
{
if (!ModelState.IsValid)
return View(vm);
if (command == "changedomain")
{
if (string.IsNullOrWhiteSpace(vm.DNSDomain))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Required field");
return View(vm);
}
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
return View(vm);
}
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.InvariantCultureIgnoreCase))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"The server is already set to use this domain");
return View(vm);
}
var builder = new UriBuilder();
using (var client = new HttpClient(new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
}))
{
try
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
if (this.Request.Host.Port != null)
builder.Port = this.Request.Host.Port.Value;
builder.Path = "runid";
builder.Query = $"expected={RunId}";
var response = await client.GetAsync(builder.Uri);
if (!response.IsSuccessStatusCode)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
}
}
catch (Exception ex)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({ex.Message})");
return View(vm);
}
}
var error = RunSSH(vm, command, $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && . changedomain.sh {vm.DNSDomain}'");
if (error != null)
return error;
builder.Path = null;
builder.Query = null;
StatusMessage = $"Domain name changing... the server will restart, please use \"{builder.Uri.AbsoluteUri}\"";
}
else
{
return NotFound();
}
return RedirectToAction(nameof(Maintenance));
}
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
[HttpGet]
[Route("runid")]
[AllowAnonymous]
public IActionResult SeeRunId(string expected = null)
{
if (expected == RunId)
return Ok();
return BadRequest();
}
private IActionResult RunSSH(MaintenanceViewModel vm, string command, string ssh)
{
var sshClient = vm.CreateSSHClient(this.Request.Host.Host);
try
{
sshClient.Connect();
}
catch (Renci.SshNet.Common.SshAuthenticationException)
{
ModelState.AddModelError(nameof(vm.Password), "Invalid credentials");
sshClient.Dispose();
return View(vm);
}
catch (Exception ex)
{
var message = ex.Message;
if (ex is AggregateException aggrEx && aggrEx.InnerException?.Message != null)
{
message = aggrEx.InnerException.Message;
}
ModelState.AddModelError(nameof(vm.UserName), $"Connection problem ({message})");
sshClient.Dispose();
return View(vm);
}
var sshCommand = sshClient.CreateCommand(ssh);
sshCommand.CommandTimeout = TimeSpan.FromMinutes(1.0);
sshCommand.BeginExecute(ar =>
{
try
{
Logs.PayServer.LogInformation("Running SSH command: " + command);
var result = sshCommand.EndExecute(ar);
Logs.PayServer.LogInformation("SSH command executed: " + result);
}
catch (Exception ex)
{
Logs.PayServer.LogWarning("Error while executing SSH command: " + ex.Message);
}
sshClient.Dispose();
});
return null;
}
private static bool IsAdmin(IList<string> roles) private static bool IsAdmin(IList<string> roles)
{ {
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal); return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
@ -336,7 +470,7 @@ namespace BTCPayServer.Controllers
if (connectionString == null) if (connectionString == null)
return null; return null;
connectionString = connectionString.Clone(); connectionString = connectionString.Clone();
if(connectionString.MacaroonFilePath != null) if (connectionString.MacaroonFilePath != null)
{ {
try try
{ {
@ -351,7 +485,7 @@ namespace BTCPayServer.Controllers
} }
return connectionString; return connectionString;
} }
[Route("server/theme")] [Route("server/theme")]
public async Task<IActionResult> Theme() public async Task<IActionResult> Theme()
{ {

View file

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Renci.SshNet;
namespace BTCPayServer.Models.ServerViewModels
{
public class MaintenanceViewModel
{
[Required]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Change domain")]
public string DNSDomain { get; set; }
public SshClient CreateSSHClient(string host)
{
return new SshClient(host, UserName, Password);
}
}
}

View file

@ -0,0 +1,52 @@
@model BTCPayServer.Models.ServerViewModels.MaintenanceViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Maintainance);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
<div class="row">
<div class="col-md-8">
<form method="post">
<div class="form-group">
<h5>SSH Settings</h5>
<span>You need SSH credentials to run any of those actions</span>
</div>
<div class="form-group">
<label asp-for="UserName"></label>
<input asp-for="UserName" class="form-control" />
<span asp-validation-for="UserName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Actions</h5>
<span>Run any of those administrative functions</span>
</div>
<div class="form-group">
<div class="input-group">
<input asp-for="DNSDomain" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary" name="command" value="changedomain" title="Change domain">
<span class="fa fa-check"></span> Confirm
</button>
</span>
</div>
<span asp-validation-for="DNSDomain" class="text-danger"></span>
</div>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View file

@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
{ {
public enum ServerNavPages public enum ServerNavPages
{ {
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintainance
} }
} }

View file

@ -5,6 +5,7 @@
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a> <a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a> <a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a> <a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintainance)" asp-action="Maintenance">Maintenance</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a> <a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
</div> </div>