btcpayserver-configurator/BTCPayServerDockerConfigurator/Controllers/ConfiguratorController.Deploy.cs
Andrew Camilleri 26c43efdf0
Some checks failed
Build and publish Docker images / build (push) Has been cancelled
Build and publish Docker images / publish (push) Has been cancelled
Add Reverse Connection deployment mode
New deployment option where the user runs a bash one-liner on their
VPS that connects back to the configurator over HTTP. The configurator
sends commands through the tunnel to read server configuration,
avoiding outbound SSH entirely. This prevents VPS abuse detection
flags from rapid SSH connect/disconnect patterns.

Architecture:
- IRemoteExecutor interface abstracts command execution over SSH
  or HTTP tunnel, letting LoadSettings work with either transport
- TunnelSession uses System.Threading.Channels for synchronized
  command/result handoff between configurator and polling agent
- TunnelService manages session lifecycle with auto-cleanup
- TunnelController serves the agent script and handles poll/result
- One-time secret per session prevents unauthorized access
- At deploy time, generates a bash script (same as Manual mode)

Bump version to 0.0.28.
2026-05-04 15:12:47 +02:00

84 lines
2.5 KiB
C#

using BTCPayServerDockerConfigurator.Models;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServerDockerConfigurator.Controllers;
public partial class ConfiguratorController
{
[HttpGet("onion")]
public async Task<IActionResult> GetOnionUrl()
{
var ssh = GetConfiguratorSettings().GetSshSettings(_options.Value, IsVerified);
using var sshC = await ssh.ConnectAsync();
var result =
await sshC.RunBash(
"cat /var/lib/docker/volumes/generated_tor_servicesdir/_data/BTCPayServer/hostname");
await sshC.DisconnectAsync();
return View(result);
}
[HttpPost("deploy")]
public IActionResult Deploy()
{
var model = GetConfiguratorSettings();
if (model.DeploymentSettings.DeploymentType is DeploymentType.Manual
or DeploymentType.ReverseConnection)
{
var id = Guid.NewGuid().ToString();
var result = new UpdateSettings<ConfiguratorSettings, DeployAdditionalData>
{
Additional = new DeployAdditionalData
{
Bash = model.ConstructBashFile(),
CloudInitScript = model.ConstructCloudInitScript(),
ExitStatus = 0
},
Json = model.ToString(),
Settings = model
};
SetTempData(id, result);
return RedirectToAction("DeployResult", new { id });
}
return RedirectToAction("DeployResult", new
{
id = _deploymentService.StartDeployment(model, IsVerified)
});
}
[HttpGet("deploy-result/{id?}")]
public IActionResult DeployResult(string id = "", string view = null, bool json = false)
{
var result = _deploymentService.GetDeploymentResult(id);
result ??=
GetTempData<UpdateSettings<ConfiguratorSettings, DeployAdditionalData>>(id);
if (result == null)
{
return RedirectToAction("Summary");
}
if (!result.Additional.InProgress)
{
SetTempData(id, result);
}
if (json)
{
return Json(result);
}
return View(view ?? "DeployResult", result);
}
}
public class DeployAdditionalData
{
public string Bash { get; set; }
public int ExitStatus { get; set; }
public string Output { get; set; }
public string Error { get; set; }
public bool InProgress { get; set; }
public string CloudInitScript { get; set; }
}