2018-07-22 11:38:14 +02:00
using BTCPayServer.Configuration ;
2018-07-23 05:20:11 +02:00
using Microsoft.Extensions.Logging ;
2018-07-22 11:38:14 +02:00
using BTCPayServer.HostedServices ;
2018-04-13 23:15:03 +02:00
using BTCPayServer.Models ;
2017-09-15 18:15:17 +02:00
using BTCPayServer.Models.ServerViewModels ;
2018-07-22 11:38:14 +02:00
using BTCPayServer.Payments.Lightning ;
2017-09-27 07:18:09 +02:00
using BTCPayServer.Services ;
using BTCPayServer.Services.Mails ;
2018-04-14 15:35:52 +02:00
using BTCPayServer.Services.Rates ;
2018-07-20 08:24:19 +02:00
using BTCPayServer.Services.Stores ;
2018-11-06 07:38:07 +01:00
using BTCPayServer.Validation ;
2017-09-15 18:20:57 +02:00
using Microsoft.AspNetCore.Authorization ;
2017-09-15 18:15:17 +02:00
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
2018-07-23 04:53:39 +02:00
using NBitcoin ;
2018-07-22 11:38:14 +02:00
using NBitcoin.DataEncoders ;
2017-09-15 18:15:17 +02:00
using System ;
using System.Collections.Generic ;
2017-09-27 07:18:09 +02:00
using System.ComponentModel.DataAnnotations ;
2018-11-07 14:29:35 +01:00
using System.IO ;
2017-09-15 18:15:17 +02:00
using System.Linq ;
2017-09-27 07:18:09 +02:00
using System.Net ;
2018-04-14 15:35:52 +02:00
using System.Net.Http ;
2017-09-27 07:18:09 +02:00
using System.Net.Mail ;
2017-09-15 18:15:17 +02:00
using System.Threading.Tasks ;
2018-07-24 10:04:57 +02:00
using Renci.SshNet ;
using BTCPayServer.Logging ;
2018-08-30 04:50:39 +02:00
using BTCPayServer.Lightning ;
2019-02-22 07:06:52 +01:00
using System.Runtime.CompilerServices ;
2019-04-22 09:41:20 +02:00
using BTCPayServer.Storage.Models ;
using BTCPayServer.Storage.Services ;
using BTCPayServer.Storage.Services.Providers ;
2019-04-12 07:13:14 +02:00
using BTCPayServer.Services.Apps ;
using Microsoft.AspNetCore.Mvc.Rendering ;
using BTCPayServer.Data ;
2017-09-15 18:15:17 +02:00
namespace BTCPayServer.Controllers
{
2018-04-29 19:33:42 +02:00
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
2019-04-22 09:41:20 +02:00
public partial class ServerController : Controller
2017-10-27 10:53:04 +02:00
{
private UserManager < ApplicationUser > _UserManager ;
SettingsRepository _SettingsRepository ;
2018-08-03 05:14:09 +02:00
private readonly NBXplorerDashboard _dashBoard ;
2018-08-22 09:53:40 +02:00
private RateFetcher _RateProviderFactory ;
2018-07-20 08:24:19 +02:00
private StoreRepository _StoreRepository ;
2018-07-22 11:38:14 +02:00
LightningConfigurationProvider _LnConfigProvider ;
2019-03-17 04:57:18 +01:00
private readonly TorServices _torServices ;
2018-07-22 11:38:14 +02:00
BTCPayServerOptions _Options ;
2019-04-12 07:43:07 +02:00
ApplicationDbContextFactory _ContextFactory ;
2019-04-22 09:41:20 +02:00
private readonly StoredFileRepository _StoredFileRepository ;
private readonly FileService _FileService ;
private readonly IEnumerable < IStorageProviderService > _StorageProviderServices ;
2017-09-15 18:15:17 +02:00
2018-04-14 15:35:52 +02:00
public ServerController ( UserManager < ApplicationUser > userManager ,
2019-04-22 09:41:20 +02:00
StoredFileRepository storedFileRepository ,
FileService fileService ,
IEnumerable < IStorageProviderService > storageProviderServices ,
2019-04-12 07:13:14 +02:00
BTCPayServerOptions options ,
2018-08-22 09:53:40 +02:00
RateFetcher rateProviderFactory ,
2018-07-20 08:24:19 +02:00
SettingsRepository settingsRepository ,
2018-08-03 05:14:09 +02:00
NBXplorerDashboard dashBoard ,
2018-12-12 10:19:13 +01:00
IHttpClientFactory httpClientFactory ,
2018-07-22 11:38:14 +02:00
LightningConfigurationProvider lnConfigProvider ,
2019-03-17 04:57:18 +01:00
TorServices torServices ,
2019-04-12 07:13:14 +02:00
StoreRepository storeRepository ,
2019-04-12 07:43:07 +02:00
ApplicationDbContextFactory contextFactory )
2017-10-27 10:53:04 +02:00
{
2018-07-22 11:38:14 +02:00
_Options = options ;
2019-04-22 09:41:20 +02:00
_StoredFileRepository = storedFileRepository ;
_FileService = fileService ;
_StorageProviderServices = storageProviderServices ;
2017-10-27 10:53:04 +02:00
_UserManager = userManager ;
_SettingsRepository = settingsRepository ;
2018-08-03 05:14:09 +02:00
_dashBoard = dashBoard ;
2018-12-12 10:19:13 +01:00
HttpClientFactory = httpClientFactory ;
2018-04-14 15:35:52 +02:00
_RateProviderFactory = rateProviderFactory ;
2018-07-20 08:24:19 +02:00
_StoreRepository = storeRepository ;
2018-07-22 11:38:14 +02:00
_LnConfigProvider = lnConfigProvider ;
2019-03-17 04:57:18 +01:00
_torServices = torServices ;
2019-04-12 07:43:07 +02:00
_ContextFactory = contextFactory ;
2017-10-27 10:53:04 +02:00
}
2017-09-15 18:15:17 +02:00
2018-04-14 15:35:52 +02:00
[Route("server/rates")]
public async Task < IActionResult > Rates ( )
{
var rates = ( await _SettingsRepository . GetSettingAsync < RatesSetting > ( ) ) ? ? new RatesSetting ( ) ;
2018-04-18 11:23:39 +02:00
var vm = new RatesViewModel ( )
2018-04-14 15:35:52 +02:00
{
CacheMinutes = rates . CacheInMinutes ,
PrivateKey = rates . PrivateKey ,
PublicKey = rates . PublicKey
2018-04-18 11:23:39 +02:00
} ;
await FetchRateLimits ( vm ) ;
return View ( vm ) ;
2018-04-14 15:35:52 +02:00
}
2018-04-18 11:23:39 +02:00
private static async Task FetchRateLimits ( RatesViewModel vm )
2018-04-14 15:35:52 +02:00
{
2018-04-18 11:23:39 +02:00
var coinAverage = GetCoinaverageService ( vm , false ) ;
if ( coinAverage ! = null )
2018-04-14 15:35:52 +02:00
{
2018-04-18 11:23:39 +02:00
try
{
vm . RateLimits = await coinAverage . GetRateLimitsAsync ( ) ;
}
catch { }
2018-04-14 15:35:52 +02:00
}
}
[Route("server/rates")]
[HttpPost]
public async Task < IActionResult > Rates ( RatesViewModel vm )
{
var rates = ( await _SettingsRepository . GetSettingAsync < RatesSetting > ( ) ) ? ? new RatesSetting ( ) ;
rates . PrivateKey = vm . PrivateKey ;
rates . PublicKey = vm . PublicKey ;
rates . CacheInMinutes = vm . CacheMinutes ;
try
{
2018-04-18 11:23:39 +02:00
var service = GetCoinaverageService ( vm , true ) ;
2018-07-22 11:38:14 +02:00
if ( service ! = null )
2018-04-18 11:23:39 +02:00
await service . TestAuthAsync ( ) ;
2018-04-14 15:35:52 +02:00
}
catch
{
ModelState . AddModelError ( nameof ( vm . PrivateKey ) , "Invalid API key pair" ) ;
}
if ( ! ModelState . IsValid )
2018-04-18 11:23:39 +02:00
{
await FetchRateLimits ( vm ) ;
2018-04-14 15:35:52 +02:00
return View ( vm ) ;
2018-04-18 11:23:39 +02:00
}
2018-04-14 15:35:52 +02:00
await _SettingsRepository . UpdateSetting ( rates ) ;
StatusMessage = "Rate settings successfully updated" ;
return RedirectToAction ( nameof ( Rates ) ) ;
2017-10-27 10:53:04 +02:00
}
2017-09-15 18:15:17 +02:00
2018-04-18 11:23:39 +02:00
private static CoinAverageRateProvider GetCoinaverageService ( RatesViewModel vm , bool withAuth )
{
var settings = new CoinAverageSettings ( )
{
KeyPair = ( vm . PublicKey , vm . PrivateKey )
} ;
if ( ! withAuth | | settings . GetCoinAverageSignature ( ) ! = null )
{
2018-05-02 20:32:42 +02:00
return new CoinAverageRateProvider ( )
2018-04-18 11:23:39 +02:00
{ Authenticator = settings } ;
}
return null ;
}
2017-10-27 10:53:04 +02:00
[Route("server/users")]
public IActionResult ListUsers ( )
{
var users = new UsersViewModel ( ) ;
2017-12-04 06:39:02 +01:00
users . StatusMessage = StatusMessage ;
2017-10-27 10:53:04 +02:00
users . Users
= _UserManager . Users . Select ( u = > new UsersViewModel . UserViewModel ( )
{
Name = u . UserName ,
2017-12-04 06:39:02 +01:00
Email = u . Email ,
Id = u . Id
2017-10-27 10:53:04 +02:00
} ) . ToList ( ) ;
return View ( users ) ;
}
2017-09-27 07:18:09 +02:00
2018-03-22 11:55:14 +01:00
[Route("server/users/{userId}")]
public new async Task < IActionResult > User ( string userId )
{
var user = await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return NotFound ( ) ;
var roles = await _UserManager . GetRolesAsync ( user ) ;
var userVM = new UserViewModel ( ) ;
userVM . Id = user . Id ;
2018-04-19 18:44:24 +02:00
userVM . Email = user . Email ;
2018-03-22 11:55:14 +01:00
userVM . IsAdmin = IsAdmin ( roles ) ;
return View ( userVM ) ;
}
2018-07-24 10:04:57 +02:00
[Route("server/maintenance")]
public IActionResult Maintenance ( )
{
MaintenanceViewModel vm = new MaintenanceViewModel ( ) ;
vm . UserName = "btcpayserver" ;
vm . DNSDomain = this . Request . Host . Host ;
2018-08-12 14:38:45 +02:00
vm . SetConfiguredSSH ( _Options . SSHSettings ) ;
2018-07-24 10:04:57 +02:00
if ( IPAddress . TryParse ( vm . DNSDomain , out var unused ) )
vm . DNSDomain = null ;
return View ( vm ) ;
}
2019-02-10 19:12:02 +01:00
2018-07-24 10:04:57 +02:00
[Route("server/maintenance")]
[HttpPost]
public async Task < IActionResult > Maintenance ( MaintenanceViewModel vm , string command )
{
if ( ! ModelState . IsValid )
return View ( vm ) ;
2018-08-12 14:38:45 +02:00
vm . SetConfiguredSSH ( _Options . SSHSettings ) ;
2018-07-24 10:04:57 +02:00
if ( command = = "changedomain" )
{
if ( string . IsNullOrWhiteSpace ( vm . DNSDomain ) )
{
ModelState . AddModelError ( nameof ( vm . DNSDomain ) , $"Required field" ) ;
return View ( vm ) ;
}
vm . DNSDomain = vm . DNSDomain . Trim ( ) . ToLowerInvariant ( ) ;
2018-08-13 09:48:10 +02:00
if ( vm . DNSDomain . Equals ( this . Request . Host . Host , StringComparison . OrdinalIgnoreCase ) )
return View ( vm ) ;
2018-07-24 10:04:57 +02:00
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
{
2018-08-13 10:04:37 +02:00
builder . Scheme = this . Request . Scheme ;
builder . Host = vm . DNSDomain ;
2019-01-30 04:52:34 +01:00
var addresses1 = GetAddressAsync ( this . Request . Host . Host ) ;
var addresses2 = GetAddressAsync ( vm . DNSDomain ) ;
2018-08-13 09:48:10 +02:00
await Task . WhenAll ( addresses1 , addresses2 ) ;
var addressesSet = addresses1 . GetAwaiter ( ) . GetResult ( ) . Select ( c = > c . ToString ( ) ) . ToHashSet ( ) ;
var hasCommonAddress = addresses2 . GetAwaiter ( ) . GetResult ( ) . Select ( c = > c . ToString ( ) ) . Any ( s = > addressesSet . Contains ( s ) ) ;
if ( ! hasCommonAddress )
2018-07-24 10:04:57 +02:00
{
ModelState . AddModelError ( nameof ( vm . DNSDomain ) , $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)" ) ;
return View ( vm ) ;
}
}
catch ( Exception ex )
{
2018-07-24 11:47:55 +02:00
var messages = new List < object > ( ) ;
messages . Add ( ex . Message ) ;
if ( ex . InnerException ! = null )
messages . Add ( ex . InnerException . Message ) ;
ModelState . AddModelError ( nameof ( vm . DNSDomain ) , $"Invalid domain ({string.Join(" , ", messages.ToArray())})" ) ;
2018-07-24 10:04:57 +02:00
return View ( vm ) ;
}
}
2018-07-24 17:51:45 +02:00
var error = RunSSH ( vm , $"changedomain.sh {vm.DNSDomain}" ) ;
2018-07-24 10:04:57 +02:00
if ( error ! = null )
return error ;
builder . Path = null ;
builder . Query = null ;
StatusMessage = $"Domain name changing... the server will restart, please use \" { builder . Uri . AbsoluteUri } \ "" ;
}
2018-07-24 15:10:37 +02:00
else if ( command = = "update" )
{
2018-07-24 18:01:05 +02:00
var error = RunSSH ( vm , $"btcpay-update.sh" ) ;
2018-07-24 15:10:37 +02:00
if ( error ! = null )
return error ;
StatusMessage = $"The server might restart soon if an update is available..." ;
}
2019-04-01 10:10:05 +02:00
else if ( command = = "clean" )
{
var error = RunSSH ( vm , $"btcpay-clean.sh" ) ;
if ( error ! = null )
return error ;
StatusMessage = $"The old docker images will be cleaned soon..." ;
}
2018-07-24 10:04:57 +02:00
else
{
return NotFound ( ) ;
}
return RedirectToAction ( nameof ( Maintenance ) ) ;
}
2019-01-30 04:52:34 +01:00
private Task < IPAddress [ ] > GetAddressAsync ( string domainOrIP )
{
if ( IPAddress . TryParse ( domainOrIP , out var ip ) )
return Task . FromResult ( new [ ] { ip } ) ;
return Dns . GetHostAddressesAsync ( domainOrIP ) ;
}
2018-07-24 10:04:57 +02:00
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 ( ) ;
}
2018-07-24 17:51:45 +02:00
private IActionResult RunSSH ( MaintenanceViewModel vm , string ssh )
2018-07-24 10:04:57 +02:00
{
2018-07-24 17:51:45 +02:00
ssh = $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && nohup {ssh} > /dev/null 2>&1 & disown'" ;
2018-08-12 14:38:45 +02:00
var sshClient = _Options . SSHSettings = = null ? vm . CreateSSHClient ( this . Request . Host . Host )
: new SshClient ( _Options . SSHSettings . CreateConnectionInfo ( ) ) ;
2018-08-12 16:23:26 +02:00
if ( _Options . TrustedFingerprints . Count ! = 0 )
{
sshClient . HostKeyReceived + = ( object sender , Renci . SshNet . Common . HostKeyEventArgs e ) = >
{
if ( _Options . TrustedFingerprints . Count = = 0 )
{
2018-08-13 02:43:59 +02:00
Logs . Configuration . LogWarning ( $"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \" { Encoders . Hex . EncodeData ( e . FingerPrint ) } \ "" ) ;
2018-08-12 16:23:26 +02:00
e . CanTrust = true ; // Not a typo, we want the connection to succeed with a warning
}
else
{
2018-08-13 02:43:59 +02:00
e . CanTrust = _Options . IsTrustedFingerprint ( e . FingerPrint , e . HostKey ) ;
2018-10-27 15:49:39 +02:00
if ( ! e . CanTrust )
2018-08-13 02:43:59 +02:00
Logs . Configuration . LogError ( $"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \" { Encoders . Hex . EncodeData ( e . FingerPrint ) } \ "" ) ;
2018-08-12 16:23:26 +02:00
}
} ;
}
else
{
}
2018-07-24 10:04:57 +02:00
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
{
2018-07-24 17:51:45 +02:00
Logs . PayServer . LogInformation ( "Running SSH command: " + ssh ) ;
2018-07-24 10:04:57 +02:00
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 ;
}
2018-03-22 11:55:14 +01:00
private static bool IsAdmin ( IList < string > roles )
{
return roles . Contains ( Roles . ServerAdmin , StringComparer . Ordinal ) ;
}
[Route("server/users/{userId}")]
[HttpPost]
public new async Task < IActionResult > User ( string userId , UserViewModel viewModel )
{
var user = await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return NotFound ( ) ;
2019-02-10 19:12:02 +01:00
viewModel . StatusMessage = "" ;
var admins = await _UserManager . GetUsersInRoleAsync ( Roles . ServerAdmin ) ;
if ( ! viewModel . IsAdmin & & admins . Count = = 1 )
{
2019-02-14 16:47:58 +01:00
viewModel . StatusMessage = "This is the only Admin, so their role can't be removed until another Admin is added." ;
2019-02-10 19:12:02 +01:00
return View ( viewModel ) ; // return
}
var roles = await _UserManager . GetRolesAsync ( user ) ;
if ( viewModel . IsAdmin ! = IsAdmin ( roles ) )
2018-03-22 11:55:14 +01:00
{
if ( viewModel . IsAdmin )
await _UserManager . AddToRoleAsync ( user , Roles . ServerAdmin ) ;
else
await _UserManager . RemoveFromRoleAsync ( user , Roles . ServerAdmin ) ;
2019-02-10 19:12:02 +01:00
2018-03-22 11:55:14 +01:00
viewModel . StatusMessage = "User successfully updated" ;
}
2019-02-10 19:12:02 +01:00
2018-03-22 11:55:14 +01:00
return View ( viewModel ) ;
}
2017-12-04 06:39:02 +01:00
[Route("server/users/{userId}/delete")]
public async Task < IActionResult > DeleteUser ( string userId )
{
var user = userId = = null ? null : await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return NotFound ( ) ;
2019-02-10 19:25:51 +01:00
var roles = await _UserManager . GetRolesAsync ( user ) ;
if ( IsAdmin ( roles ) )
{
2019-03-03 03:01:32 +01:00
var admins = await _UserManager . GetUsersInRoleAsync ( Roles . ServerAdmin ) ;
if ( admins . Count = = 1 )
{
// return
return View ( "Confirm" , new ConfirmModel ( "Unable to Delete Last Admin" ,
"This is the last Admin, so it can't be removed" ) ) ;
}
2019-02-10 19:25:51 +01:00
return View ( "Confirm" , new ConfirmModel ( "Delete Admin " + user . Email ,
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?" ,
"Delete" ) ) ;
}
else
{
return View ( "Confirm" , new ConfirmModel ( "Delete user " + user . Email ,
"This user will be permanently deleted" ,
"Delete" ) ) ;
}
2017-12-04 06:39:02 +01:00
}
[Route("server/users/{userId}/delete")]
[HttpPost]
public async Task < IActionResult > DeleteUserPost ( string userId )
{
var user = userId = = null ? null : await _UserManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return NotFound ( ) ;
await _UserManager . DeleteAsync ( user ) ;
2018-07-20 08:24:19 +02:00
await _StoreRepository . CleanUnreachableStores ( ) ;
2017-12-04 06:39:02 +01:00
StatusMessage = "User deleted" ;
return RedirectToAction ( nameof ( ListUsers ) ) ;
}
[TempData]
public string StatusMessage
{
get ; set ;
}
2018-12-12 10:19:13 +01:00
public IHttpClientFactory HttpClientFactory { get ; }
2017-12-04 06:39:02 +01:00
2017-10-27 10:53:04 +02:00
[Route("server/policies")]
public async Task < IActionResult > Policies ( )
{
var data = ( await _SettingsRepository . GetSettingAsync < PoliciesSettings > ( ) ) ? ? new PoliciesSettings ( ) ;
2019-04-12 07:13:14 +02:00
2019-04-12 07:43:07 +02:00
// load display app dropdown
using ( var ctx = _ContextFactory . CreateContext ( ) )
{
2019-04-12 07:54:59 +02:00
var userId = _UserManager . GetUserId ( base . User ) ;
var selectList = ctx . Users . Where ( user = > user . Id = = userId )
. SelectMany ( s = > s . UserStores )
. Select ( s = > s . StoreData )
. SelectMany ( s = > s . Apps )
. Select ( a = > new SelectListItem ( $"{a.AppType} - {a.Name}" , a . Id ) ) . ToList ( ) ;
2019-04-12 07:43:07 +02:00
selectList . Insert ( 0 , new SelectListItem ( "(None)" , null ) ) ;
ViewBag . AppsList = new SelectList ( selectList , "Value" , "Text" , data . RootAppId ) ;
}
2017-10-27 10:53:04 +02:00
return View ( data ) ;
}
[Route("server/policies")]
[HttpPost]
public async Task < IActionResult > Policies ( PoliciesSettings settings )
2018-04-19 18:39:51 +02:00
{
2019-04-12 07:13:14 +02:00
if ( ! String . IsNullOrEmpty ( settings . RootAppId ) )
{
2019-04-12 07:43:07 +02:00
using ( var ctx = _ContextFactory . CreateContext ( ) )
2019-04-12 07:13:14 +02:00
{
2019-04-12 07:43:07 +02:00
var app = ctx . Apps . SingleOrDefault ( a = > a . Id = = settings . RootAppId ) ;
if ( app ! = null )
settings . RootAppType = Enum . Parse < AppType > ( app . AppType ) ;
2019-04-12 07:54:59 +02:00
else
settings . RootAppType = null ;
2019-04-12 07:13:14 +02:00
}
}
else
{
// not preserved on client side, but clearing it just in case
settings . RootAppType = null ;
}
2018-04-19 18:39:51 +02:00
await _SettingsRepository . UpdateSetting ( settings ) ;
TempData [ "StatusMessage" ] = "Policies updated successfully" ;
2019-04-12 07:13:14 +02:00
return RedirectToAction ( nameof ( Policies ) ) ;
2018-04-19 18:39:51 +02:00
}
2018-07-22 11:38:14 +02:00
[Route("server/services")]
2019-04-22 09:41:20 +02:00
public async Task < IActionResult > Services ( )
2018-07-22 11:38:14 +02:00
{
var result = new ServicesViewModel ( ) ;
2019-05-07 07:31:49 +02:00
result . ExternalServices = _Options . ExternalServices . ToList ( ) ;
2019-03-01 05:20:21 +01:00
foreach ( var externalService in _Options . OtherExternalServices )
2018-07-22 11:38:14 +02:00
{
2019-03-01 05:20:21 +01:00
result . OtherExternalServices . Add ( new ServicesViewModel . OtherExternalService ( )
2018-12-07 10:42:39 +01:00
{
Name = externalService . Key ,
2019-03-01 06:46:32 +01:00
Link = this . Request . GetAbsoluteUriNoPathBase ( externalService . Value ) . AbsoluteUri
2018-12-07 10:42:39 +01:00
} ) ;
}
2019-02-10 19:12:02 +01:00
if ( _Options . SSHSettings ! = null )
2018-12-07 10:42:39 +01:00
{
2019-03-01 05:20:21 +01:00
result . OtherExternalServices . Add ( new ServicesViewModel . OtherExternalService ( )
2018-12-07 10:42:39 +01:00
{
Name = "SSH" ,
Link = this . Url . Action ( nameof ( SSHService ) )
} ) ;
}
2019-04-12 07:13:14 +02:00
foreach ( var torService in _torServices . Services )
2019-03-17 12:49:26 +01:00
{
if ( torService . VirtualPort = = 80 )
{
2019-03-18 08:45:46 +01:00
result . TorHttpServices . Add ( new ServicesViewModel . OtherExternalService ( )
2019-03-17 12:49:26 +01:00
{
Name = torService . Name ,
Link = $"http://{torService.OnionHost}"
} ) ;
}
2019-05-07 06:58:55 +02:00
else if ( TryParseAsExternalService ( torService , out var externalService ) )
{
result . ExternalServices . Add ( externalService ) ;
}
2019-03-18 08:45:46 +01:00
else
{
2019-03-18 09:13:02 +01:00
result . TorOtherServices . Add ( new ServicesViewModel . OtherExternalService ( )
2019-03-18 08:45:46 +01:00
{
Name = torService . Name ,
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
} ) ;
}
2019-03-17 12:49:26 +01:00
}
2019-04-22 09:41:20 +02:00
var storageSettings = await _SettingsRepository . GetSettingAsync < StorageSettings > ( ) ;
result . ExternalStorageServices . Add ( new ServicesViewModel . OtherExternalService ( )
{
Name = storageSettings = = null ? "Not set" : storageSettings . Provider . ToString ( ) ,
Link = Url . Action ( "Storage" )
} ) ;
2018-07-22 11:38:14 +02:00
return View ( result ) ;
}
2019-05-07 06:58:55 +02:00
private static bool TryParseAsExternalService ( TorService torService , out ExternalService externalService )
{
externalService = null ;
if ( torService . ServiceType = = TorServiceType . P2P )
{
externalService = new ExternalService ( )
{
CryptoCode = torService . Network . CryptoCode ,
DisplayName = "Full node P2P" ,
Type = ExternalServiceTypes . P2P ,
ConnectionString = new ExternalConnectionString ( new Uri ( $"bitcoin-p2p://{torService.OnionHost}:{torService.VirtualPort}" , UriKind . Absolute ) ) ,
ServiceName = torService . Name ,
} ;
}
return externalService ! = null ;
}
private ExternalService GetService ( string serviceName , string cryptoCode )
{
var result = _Options . ExternalServices . GetService ( serviceName , cryptoCode ) ;
if ( result ! = null )
return result ;
_torServices . Services . FirstOrDefault ( s = > TryParseAsExternalService ( s , out result ) ) ;
return result ;
}
2019-03-01 05:20:21 +01:00
[Route("server/services/{serviceName}/{cryptoCode}")]
public async Task < IActionResult > Service ( string serviceName , string cryptoCode , bool showQR = false , uint? nonce = null )
2018-12-20 14:40:32 +01:00
{
if ( ! _dashBoard . IsFullySynched ( cryptoCode , out var unusud ) )
{
StatusMessage = $"Error: {cryptoCode} is not fully synched" ;
return RedirectToAction ( nameof ( Services ) ) ;
}
2019-05-07 06:58:55 +02:00
var service = GetService ( serviceName , cryptoCode ) ;
2019-03-01 05:20:21 +01:00
if ( service = = null )
2018-12-20 14:40:32 +01:00
return NotFound ( ) ;
try
{
2019-05-07 06:58:55 +02:00
if ( service . Type = = ExternalServiceTypes . P2P )
{
return View ( "P2PService" , new LightningWalletServices ( )
{
ShowQR = showQR ,
WalletName = service . ServiceName ,
2019-05-07 07:44:26 +02:00
ServiceLink = service . ConnectionString . Server . AbsoluteUri . WithoutEndingSlash ( )
2019-05-07 06:58:55 +02:00
} ) ;
}
2019-03-01 05:20:21 +01:00
var connectionString = await service . ConnectionString . Expand ( this . Request . GetAbsoluteUriNoPathBase ( ) , service . Type ) ;
switch ( service . Type )
2018-12-20 14:40:32 +01:00
{
2019-03-01 05:20:21 +01:00
case ExternalServiceTypes . Charge :
return LightningChargeServices ( service , connectionString , showQR ) ;
case ExternalServiceTypes . RTL :
case ExternalServiceTypes . Spark :
if ( connectionString . AccessKey = = null )
{
StatusMessage = $"Error: The access key of the service is not set" ;
return RedirectToAction ( nameof ( Services ) ) ;
}
LightningWalletServices vm = new LightningWalletServices ( ) ;
vm . ShowQR = showQR ;
vm . WalletName = service . DisplayName ;
vm . ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}" ;
return View ( "LightningWalletServices" , vm ) ;
case ExternalServiceTypes . LNDGRPC :
case ExternalServiceTypes . LNDRest :
return LndServices ( service , connectionString , nonce ) ;
default :
throw new NotSupportedException ( service . Type . ToString ( ) ) ;
2019-02-28 14:20:14 +01:00
}
2018-12-12 10:19:13 +01:00
}
2019-02-10 19:12:02 +01:00
catch ( Exception ex )
2018-12-12 10:19:13 +01:00
{
StatusMessage = $"Error: {ex.Message}" ;
return RedirectToAction ( nameof ( Services ) ) ;
}
}
2019-03-01 05:20:21 +01:00
private IActionResult LightningChargeServices ( ExternalService service , ExternalConnectionString connectionString , bool showQR = false )
2019-02-28 14:20:14 +01:00
{
2019-03-01 05:20:21 +01:00
ChargeServiceViewModel vm = new ChargeServiceViewModel ( ) ;
vm . Uri = connectionString . Server . AbsoluteUri ;
vm . APIToken = connectionString . APIToken ;
2019-03-01 07:38:11 +01:00
var builder = new UriBuilder ( connectionString . Server ) ;
builder . UserName = "api-token" ;
builder . Password = vm . APIToken ;
vm . AuthenticatedUri = builder . ToString ( ) ;
2019-03-01 05:20:21 +01:00
return View ( nameof ( LightningChargeServices ) , vm ) ;
2019-02-28 14:20:14 +01:00
}
2019-03-01 05:20:21 +01:00
private IActionResult LndServices ( ExternalService service , ExternalConnectionString connectionString , uint? nonce )
2018-07-22 11:38:14 +02:00
{
2018-10-27 15:49:39 +02:00
var model = new LndGrpcServicesViewModel ( ) ;
2019-03-01 05:20:21 +01:00
if ( service . Type = = ExternalServiceTypes . LNDGRPC )
2018-12-07 11:31:07 +01:00
{
2019-03-01 05:20:21 +01:00
model . Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}" ;
model . SSL = connectionString . Server . Scheme = = "https" ;
2018-12-07 11:31:07 +01:00
model . ConnectionType = "GRPC" ;
2018-12-20 06:16:23 +01:00
model . GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256" ;
2018-12-07 11:31:07 +01:00
}
2019-03-01 05:20:21 +01:00
else if ( service . Type = = ExternalServiceTypes . LNDRest )
2018-12-07 11:31:07 +01:00
{
2019-03-01 05:20:21 +01:00
model . Uri = connectionString . Server . AbsoluteUri ;
2018-12-07 11:31:07 +01:00
model . ConnectionType = "REST" ;
}
2018-07-22 11:38:14 +02:00
2019-03-01 05:20:21 +01:00
if ( connectionString . CertificateThumbprint ! = null )
2018-07-22 11:38:14 +02:00
{
2019-03-01 05:20:21 +01:00
model . CertificateThumbprint = connectionString . CertificateThumbprint ;
2018-07-22 11:38:14 +02:00
}
2019-03-01 05:20:21 +01:00
if ( connectionString . Macaroon ! = null )
2018-07-22 11:38:14 +02:00
{
2019-03-01 05:20:21 +01:00
model . Macaroon = Encoders . Hex . EncodeData ( connectionString . Macaroon ) ;
2018-07-22 11:38:14 +02:00
}
2019-03-01 05:20:21 +01:00
model . AdminMacaroon = connectionString . Macaroons ? . AdminMacaroon ? . Hex ;
model . InvoiceMacaroon = connectionString . Macaroons ? . InvoiceMacaroon ? . Hex ;
model . ReadonlyMacaroon = connectionString . Macaroons ? . ReadonlyMacaroon ? . Hex ;
2018-07-23 04:53:39 +02:00
if ( nonce ! = null )
2018-07-22 11:38:14 +02:00
{
2019-03-01 05:20:21 +01:00
var configKey = GetConfigKey ( "lnd" , service . ServiceName , service . CryptoCode , nonce . Value ) ;
2018-07-23 04:53:39 +02:00
var lnConfig = _LnConfigProvider . GetConfig ( configKey ) ;
2018-07-22 11:38:14 +02:00
if ( lnConfig ! = null )
{
2019-03-07 02:26:27 +01:00
model . QRCodeLink = Request . GetAbsoluteUri ( Url . Action ( nameof ( GetLNDConfig ) , new { configKey = configKey } ) ) ;
2018-07-22 14:28:21 +02:00
model . QRCode = $"config={model.QRCodeLink}" ;
2018-07-22 11:38:14 +02:00
}
}
2018-07-23 04:53:39 +02:00
2019-03-01 05:20:21 +01:00
return View ( nameof ( LndServices ) , model ) ;
2018-07-22 11:38:14 +02:00
}
2018-07-23 04:53:39 +02:00
2019-03-01 05:20:21 +01:00
private static uint GetConfigKey ( string type , string serviceName , string cryptoCode , uint nonce )
2018-07-23 04:53:39 +02:00
{
2019-03-01 05:20:21 +01:00
return ( uint ) HashCode . Combine ( type , serviceName , cryptoCode , nonce ) ;
2018-07-23 04:53:39 +02:00
}
[Route("lnd-config/{configKey}/lnd.config")]
2018-07-22 11:43:11 +02:00
[AllowAnonymous]
2018-07-23 04:53:39 +02:00
public IActionResult GetLNDConfig ( uint configKey )
2018-07-22 11:38:14 +02:00
{
2018-07-23 04:53:39 +02:00
var conf = _LnConfigProvider . GetConfig ( configKey ) ;
2018-07-22 11:38:14 +02:00
if ( conf = = null )
return NotFound ( ) ;
return Json ( conf ) ;
}
2019-03-01 05:20:21 +01:00
[Route("server/services/{serviceName}/{cryptoCode}")]
2018-07-22 11:38:14 +02:00
[HttpPost]
2019-03-01 05:20:21 +01:00
public async Task < IActionResult > ServicePost ( string serviceName , string cryptoCode )
2018-07-22 11:38:14 +02:00
{
2019-03-01 05:20:21 +01:00
if ( ! _dashBoard . IsFullySynched ( cryptoCode , out var unusud ) )
{
StatusMessage = $"Error: {cryptoCode} is not fully synched" ;
return RedirectToAction ( nameof ( Services ) ) ;
}
2019-05-07 06:58:55 +02:00
var service = GetService ( serviceName , cryptoCode ) ;
2019-03-01 05:20:21 +01:00
if ( service = = null )
2018-07-22 11:38:14 +02:00
return NotFound ( ) ;
2019-03-01 05:20:21 +01:00
ExternalConnectionString connectionString = null ;
try
{
connectionString = await service . ConnectionString . Expand ( this . Request . GetAbsoluteUriNoPathBase ( ) , service . Type ) ;
}
catch ( Exception ex )
{
StatusMessage = $"Error: {ex.Message}" ;
return RedirectToAction ( nameof ( Services ) ) ;
}
2018-07-22 11:38:14 +02:00
LightningConfigurations confs = new LightningConfigurations ( ) ;
2019-03-01 05:20:21 +01:00
if ( service . Type = = ExternalServiceTypes . LNDGRPC )
2018-12-07 11:31:07 +01:00
{
2018-12-20 08:52:04 +01:00
LightningConfiguration grpcConf = new LightningConfiguration ( ) ;
grpcConf . Type = "grpc" ;
2019-03-01 05:20:21 +01:00
grpcConf . Host = connectionString . Server . DnsSafeHost ;
grpcConf . Port = connectionString . Server . Port ;
grpcConf . SSL = connectionString . Server . Scheme = = "https" ;
2018-12-20 08:52:04 +01:00
confs . Configurations . Add ( grpcConf ) ;
2018-12-07 11:31:07 +01:00
}
2019-03-01 05:20:21 +01:00
else if ( service . Type = = ExternalServiceTypes . LNDRest )
2018-12-07 11:31:07 +01:00
{
var restconf = new LNDRestConfiguration ( ) ;
restconf . Type = "lnd-rest" ;
2019-03-01 05:20:21 +01:00
restconf . Uri = connectionString . Server . AbsoluteUri ;
2018-12-07 11:31:07 +01:00
confs . Configurations . Add ( restconf ) ;
}
2018-12-20 08:52:04 +01:00
else
2019-03-01 05:20:21 +01:00
throw new NotSupportedException ( service . Type . ToString ( ) ) ;
2018-12-20 08:52:04 +01:00
var commonConf = ( LNDConfiguration ) confs . Configurations [ confs . Configurations . Count - 1 ] ;
commonConf . ChainType = _Options . NetworkType . ToString ( ) ;
commonConf . CryptoCode = cryptoCode ;
2019-03-01 05:20:21 +01:00
commonConf . Macaroon = connectionString . Macaroon = = null ? null : Encoders . Hex . EncodeData ( connectionString . Macaroon ) ;
commonConf . CertificateThumbprint = connectionString . CertificateThumbprint = = null ? null : connectionString . CertificateThumbprint ;
commonConf . AdminMacaroon = connectionString . Macaroons ? . AdminMacaroon ? . Hex ;
commonConf . ReadonlyMacaroon = connectionString . Macaroons ? . ReadonlyMacaroon ? . Hex ;
commonConf . InvoiceMacaroon = connectionString . Macaroons ? . InvoiceMacaroon ? . Hex ;
2018-12-20 08:52:04 +01:00
2018-07-23 04:53:39 +02:00
var nonce = RandomUtils . GetUInt32 ( ) ;
2019-03-01 05:20:21 +01:00
var configKey = GetConfigKey ( "lnd" , serviceName , cryptoCode , nonce ) ;
2018-07-23 04:53:39 +02:00
_LnConfigProvider . KeepConfig ( configKey , confs ) ;
2019-03-01 05:20:21 +01:00
return RedirectToAction ( nameof ( Service ) , new { cryptoCode = cryptoCode , serviceName = serviceName , nonce = nonce } ) ;
2018-07-22 11:38:14 +02:00
}
2018-07-24 10:04:57 +02:00
2018-08-12 14:38:45 +02:00
[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" ) ;
}
2019-03-01 08:41:36 +01:00
2019-04-12 07:13:14 +02:00
var server = Extensions . IsLocalNetwork ( settings . Server ) ? this . Request . Host . Host : settings . Server ;
2018-08-12 14:38:45 +02:00
SSHServiceViewModel vm = new SSHServiceViewModel ( ) ;
string port = settings . Port = = 22 ? "" : $" -p {settings.Port}" ;
2019-03-01 08:41:36 +01:00
vm . CommandLine = $"ssh {settings.Username}@{server}{port}" ;
2018-08-12 14:38:45 +02:00
vm . Password = settings . Password ;
vm . KeyFilePassword = settings . KeyFilePassword ;
vm . HasKeyFile = ! string . IsNullOrEmpty ( settings . KeyFile ) ;
return View ( vm ) ;
}
2018-04-19 18:39:51 +02:00
[Route("server/theme")]
public async Task < IActionResult > Theme ( )
{
var data = ( await _SettingsRepository . GetSettingAsync < ThemeSettings > ( ) ) ? ? new ThemeSettings ( ) ;
return View ( data ) ;
}
[Route("server/theme")]
[HttpPost]
public async Task < IActionResult > Theme ( ThemeSettings settings )
2017-10-27 10:53:04 +02:00
{
await _SettingsRepository . UpdateSetting ( settings ) ;
2018-04-19 18:39:51 +02:00
TempData [ "StatusMessage" ] = "Theme settings updated successfully" ;
2017-10-27 10:53:04 +02:00
return View ( settings ) ;
}
2017-09-27 07:18:09 +02:00
2019-03-04 22:56:23 +01:00
[Route("server/emails")]
public async Task < IActionResult > Emails ( )
{
var data = ( await _SettingsRepository . GetSettingAsync < EmailSettings > ( ) ) ? ? new EmailSettings ( ) ;
return View ( new EmailsViewModel ( ) { Settings = data } ) ;
}
2017-10-27 10:53:04 +02:00
[Route("server/emails")]
[HttpPost]
public async Task < IActionResult > Emails ( EmailsViewModel model , string command )
{
2019-03-04 22:56:23 +01:00
if ( ! model . Settings . IsComplete ( ) )
{
model . StatusMessage = "Error: Required fields missing" ;
return View ( model ) ;
}
2017-10-27 10:53:04 +02:00
if ( command = = "Test" )
{
try
{
var client = model . Settings . CreateSmtpClient ( ) ;
await client . SendMailAsync ( model . Settings . From , model . TestEmail , "BTCPay test" , "BTCPay test" ) ;
model . StatusMessage = "Email sent to " + model . TestEmail + ", please, verify you received it" ;
}
catch ( Exception ex )
{
model . StatusMessage = "Error: " + ex . Message ;
}
return View ( model ) ;
}
2018-05-04 08:54:12 +02:00
else // if(command == "Save")
2017-10-27 10:53:04 +02:00
{
await _SettingsRepository . UpdateSetting ( model . Settings ) ;
model . StatusMessage = "Email settings saved" ;
return View ( model ) ;
}
}
2018-11-07 14:29:35 +01:00
[Route("server/logs/{file?}")]
public async Task < IActionResult > LogsView ( string file = null , int offset = 0 )
{
if ( offset < 0 )
{
offset = 0 ;
}
var vm = new LogsViewModel ( ) ;
if ( string . IsNullOrEmpty ( _Options . LogFile ) )
{
vm . StatusMessage = "Error: File Logging Option not specified. " +
"You need to set debuglog and optionally " +
"debugloglevel in the configuration or through runtime arguments" ;
}
else
{
var di = Directory . GetParent ( _Options . LogFile ) ;
if ( di = = null )
{
vm . StatusMessage = "Error: Could not load log files" ;
}
var fileNameWithoutExtension = Path . GetFileNameWithoutExtension ( _Options . LogFile ) ;
var fileExtension = Path . GetExtension ( _Options . LogFile ) ? ? string . Empty ;
var logFiles = di . GetFiles ( $"{fileNameWithoutExtension}*{fileExtension}" ) ;
vm . LogFileCount = logFiles . Length ;
vm . LogFiles = logFiles
. OrderBy ( info = > info . LastWriteTime )
. Skip ( offset )
. Take ( 5 )
. ToList ( ) ;
vm . LogFileOffset = offset ;
2019-02-10 19:12:02 +01:00
if ( string . IsNullOrEmpty ( file ) )
return View ( "Logs" , vm ) ;
2018-11-07 14:29:35 +01:00
vm . Log = "" ;
var path = Path . Combine ( di . FullName , file ) ;
2018-11-09 13:43:10 +01:00
try
2018-11-07 14:29:35 +01:00
{
2018-11-09 13:43:10 +01:00
using ( var fileStream = new FileStream (
path ,
FileMode . Open ,
FileAccess . Read ,
FileShare . ReadWrite ) )
2018-11-07 14:29:35 +01:00
{
2018-11-09 13:43:10 +01:00
using ( var reader = new StreamReader ( fileStream ) )
{
vm . Log = await reader . ReadToEndAsync ( ) ;
}
2018-11-07 14:29:35 +01:00
}
}
2018-11-09 13:43:10 +01:00
catch
{
return NotFound ( ) ;
}
2018-11-07 14:29:35 +01:00
}
return View ( "Logs" , vm ) ;
}
2017-10-27 10:53:04 +02:00
}
2017-09-15 18:15:17 +02:00
}