Can edit authorized_keys in SSH Services, remove download keyfile support

This commit is contained in:
nicolas.dorier
2019-09-19 19:17:20 +09:00
parent 42dda56eea
commit a2cb6178b8
6 changed files with 83 additions and 16 deletions

View File

@@ -77,7 +77,7 @@ namespace BTCPayServer.Tests
public string RegisterNewUser(bool isAdmin = false) public string RegisterNewUser(bool isAdmin = false)
{ {
var usr = RandomUtils.GetUInt256().ToString() + "@a.com"; var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
Driver.FindElement(By.Id("Register")).Click(); Driver.FindElement(By.Id("Register")).Click();
Driver.FindElement(By.Id("Email")).SendKeys(usr); Driver.FindElement(By.Id("Email")).SendKeys(usr);
Driver.FindElement(By.Id("Password")).SendKeys("123456"); Driver.FindElement(By.Id("Password")).SendKeys("123456");

View File

@@ -113,6 +113,16 @@ namespace BTCPayServer.Tests
} }
s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh")); s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh"));
s.Driver.AssertNoError(); s.Driver.AssertNoError();
s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear();
s.Driver.FindElement(By.Id("SSHKeyFileContent")).SendKeys("tes't\r\ntest2");
s.Driver.FindElement(By.Id("submit")).ForceClick();
s.Driver.AssertNoError();
var text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text;
// Browser replace \n to \r\n, so it is hard to compare exactly what we want
Assert.Contains("tes't", text);
Assert.Contains("test2", text);
Assert.True(s.Driver.PageSource.Contains("authorized_keys has been updated", StringComparison.OrdinalIgnoreCase));
} }
} }

View File

@@ -898,17 +898,11 @@ namespace BTCPayServer.Controllers
} }
[Route("server/services/ssh")] [Route("server/services/ssh")]
public IActionResult SSHService(bool downloadKeyFile = false) public async Task<IActionResult> SSHService()
{ {
var settings = _Options.SSHSettings; var settings = _Options.SSHSettings;
if (settings == null) if (settings == null)
return NotFound(); 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");
}
var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server; var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server;
SSHServiceViewModel vm = new SSHServiceViewModel(); SSHServiceViewModel vm = new SSHServiceViewModel();
@@ -917,9 +911,46 @@ namespace BTCPayServer.Controllers
vm.Password = settings.Password; vm.Password = settings.Password;
vm.KeyFilePassword = settings.KeyFilePassword; vm.KeyFilePassword = settings.KeyFilePassword;
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile); vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
vm.CanConnect = _sshState.CanUseSSH;
if (vm.CanConnect)
{
try
{
using (var sshClient = await _Options.SSHSettings.ConnectAsync())
{
var result = await sshClient.RunBash("cat ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10));
vm.SSHKeyFileContent = result.Output;
}
}
catch
{
}
}
return View(vm); return View(vm);
} }
[HttpPost]
[Route("server/services/ssh")]
public async Task<IActionResult> SSHService(SSHServiceViewModel viewModel)
{
string newContent = viewModel?.SSHKeyFileContent ?? string.Empty;
newContent = newContent.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase);
try
{
using (var sshClient = await _Options.SSHSettings.ConnectAsync())
{
await sshClient.RunBash($"mkdir -p ~/.ssh && echo '{newContent.EscapeSingleQuotes()}' > ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10));
}
StatusMessage = "authorized_keys has been updated";
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
}
return RedirectToAction(nameof(SSHService));
}
[Route("server/theme")] [Route("server/theme")]
public async Task<IActionResult> Theme() public async Task<IActionResult> Theme()
{ {

View File

@@ -49,13 +49,18 @@ namespace BTCPayServer
return tcs.Task; return tcs.Task;
} }
public static string EscapeSingleQuotes(this string command)
{
return command.Replace("'", "'\"'\"'", StringComparison.OrdinalIgnoreCase);
}
public static Task<SSHCommandResult> RunBash(this SshClient sshClient, string command, TimeSpan? timeout = null) public static Task<SSHCommandResult> RunBash(this SshClient sshClient, string command, TimeSpan? timeout = null)
{ {
if (sshClient == null) if (sshClient == null)
throw new ArgumentNullException(nameof(sshClient)); throw new ArgumentNullException(nameof(sshClient));
if (command == null) if (command == null)
throw new ArgumentNullException(nameof(command)); throw new ArgumentNullException(nameof(command));
command = $"sudo bash -c '{command}'"; command = $"bash -c '{command.EscapeSingleQuotes()}'";
var sshCommand = sshClient.CreateCommand(command); var sshCommand = sshClient.CreateCommand(command);
if (timeout is TimeSpan v) if (timeout is TimeSpan v)
sshCommand.CommandTimeout = v; sshCommand.CommandTimeout = v;

View File

@@ -11,5 +11,7 @@ namespace BTCPayServer.Models.ServerViewModels
public string Password { get; set; } public string Password { get; set; }
public string KeyFilePassword { get; set; } public string KeyFilePassword { get; set; }
public bool HasKeyFile { get; set; } public bool HasKeyFile { get; set; }
public bool CanConnect { get; set; }
public string SSHKeyFileContent { get; set; }
} }
} }

View File

@@ -5,7 +5,7 @@
<h4>SSH settings</h4> <h4>SSH settings</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" /> <partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div> <div asp-validation-summary="All" class="text-danger"></div>
@@ -41,10 +41,29 @@
<input asp-for="KeyFilePassword" readonly class="form-control" /> <input asp-for="KeyFilePassword" readonly class="form-control" />
</div> </div>
} }
@if(Model.HasKeyFile) </div>
</div>
</div>
@if (Model.CanConnect)
{ {
<a class="btn btn-primary form-control" asp-action="SSHService" asp-route-downloadKeyFile="true">Download Key File</a> <h4>Authorized keys</h4>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>
<span>You can enter here SSH public keys authorized to connect to your server.<br /></span>
</p>
</div>
<form method="post">
<div class="form-group">
<textarea asp-for="SSHKeyFileContent" rows="20" cols="80" class="form-control"></textarea>
<span asp-validation-for="SSHKeyFileContent" class="text-danger"></span>
</div>
<button id="submit" type="submit" class="btn btn-primary" value="Save">Save</button>
</form>
</div>
</div>
} }
</div>
</div>
</div>