diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 3507b6b9f..3fc4bf8e4 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -77,7 +77,7 @@ namespace BTCPayServer.Tests 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("Email")).SendKeys(usr); Driver.FindElement(By.Id("Password")).SendKeys("123456"); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index dddc218ba..1902aef10 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -113,6 +113,16 @@ namespace BTCPayServer.Tests } s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh")); 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)); } } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index c6fc646ce..47c61b7ad 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -898,17 +898,11 @@ namespace BTCPayServer.Controllers } [Route("server/services/ssh")] - public IActionResult SSHService(bool downloadKeyFile = false) + public async Task SSHService() { 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"); - } var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server; SSHServiceViewModel vm = new SSHServiceViewModel(); @@ -917,9 +911,46 @@ namespace BTCPayServer.Controllers vm.Password = settings.Password; vm.KeyFilePassword = settings.KeyFilePassword; 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); } + [HttpPost] + [Route("server/services/ssh")] + public async Task 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")] public async Task Theme() { diff --git a/BTCPayServer/Extensions/SSHClientExtensions.cs b/BTCPayServer/Extensions/SSHClientExtensions.cs index 05d15d81d..6a7d0b4d2 100644 --- a/BTCPayServer/Extensions/SSHClientExtensions.cs +++ b/BTCPayServer/Extensions/SSHClientExtensions.cs @@ -49,13 +49,18 @@ namespace BTCPayServer return tcs.Task; } + public static string EscapeSingleQuotes(this string command) + { + return command.Replace("'", "'\"'\"'", StringComparison.OrdinalIgnoreCase); + } + public static Task RunBash(this SshClient sshClient, string command, TimeSpan? timeout = null) { if (sshClient == null) throw new ArgumentNullException(nameof(sshClient)); if (command == null) throw new ArgumentNullException(nameof(command)); - command = $"sudo bash -c '{command}'"; + command = $"bash -c '{command.EscapeSingleQuotes()}'"; var sshCommand = sshClient.CreateCommand(command); if (timeout is TimeSpan v) sshCommand.CommandTimeout = v; diff --git a/BTCPayServer/Models/ServerViewModels/SSHServiceViewModel.cs b/BTCPayServer/Models/ServerViewModels/SSHServiceViewModel.cs index 788b49c43..cfb893dc0 100644 --- a/BTCPayServer/Models/ServerViewModels/SSHServiceViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/SSHServiceViewModel.cs @@ -11,5 +11,7 @@ namespace BTCPayServer.Models.ServerViewModels public string Password { get; set; } public string KeyFilePassword { get; set; } public bool HasKeyFile { get; set; } + public bool CanConnect { get; set; } + public string SSHKeyFileContent { get; set; } } } diff --git a/BTCPayServer/Views/Server/SSHService.cshtml b/BTCPayServer/Views/Server/SSHService.cshtml index 4956d9062..7c99be3b0 100644 --- a/BTCPayServer/Views/Server/SSHService.cshtml +++ b/BTCPayServer/Views/Server/SSHService.cshtml @@ -5,7 +5,7 @@

SSH settings

- +
@@ -27,24 +27,43 @@
- @if(!string.IsNullOrEmpty(Model.Password)) + @if (!string.IsNullOrEmpty(Model.Password)) {
} - @if(!string.IsNullOrEmpty(Model.KeyFilePassword)) + @if (!string.IsNullOrEmpty(Model.KeyFilePassword)) {
} - @if(Model.HasKeyFile) - { - Download Key File - }
+ +@if (Model.CanConnect) +{ +

Authorized keys

+
+ +
+
+

+ You can enter here SSH public keys authorized to connect to your server.
+

+
+ +
+
+ + +
+ +
+
+
+}