From ce055dece91a2b19dff6fe172a1a7c154ba0553d Mon Sep 17 00:00:00 2001 From: NicolasDorier Date: Sat, 23 Sep 2017 01:31:29 +0900 Subject: [PATCH] .NET 2.0sify, use the same configuration framework as NBXplorer --- BTCPayServer.Tests/BTCPayServerTester.cs | 5 +- BTCPayServer.Tests/NBXplorerTester.cs | 2 +- BTCPayServer/BTCPayServer.csproj | 3 + .../Configuration/BTCPayServerOptions.cs | 164 +------------ .../Configuration/BTCPayServerRuntime.cs | 2 +- BTCPayServer/Configuration/ConfigException.cs | 15 ++ .../Configuration/ConfigurationExtensions.cs | 52 +++++ .../Configuration/DefaultConfiguration.cs | 96 ++++++++ .../Configuration/DefaultDataDirectory.cs | 52 ----- .../Configuration/NetworkInformation.cs | 103 ++++++++ .../Configuration/TextFileConfiguration.cs | 221 ------------------ BTCPayServer/Extensions.cs | 14 ++ BTCPayServer/Hosting/BTCPayServerServices.cs | 182 ++++++++++----- BTCPayServer/Hosting/Startup.cs | 82 ++----- BTCPayServer/Program.cs | 56 ++--- BTCPayServer/wwwroot/img/ibuki.png | Bin 11406 -> 1431 bytes 16 files changed, 465 insertions(+), 584 deletions(-) create mode 100644 BTCPayServer/Configuration/ConfigException.cs create mode 100644 BTCPayServer/Configuration/ConfigurationExtensions.cs create mode 100644 BTCPayServer/Configuration/DefaultConfiguration.cs delete mode 100644 BTCPayServer/Configuration/DefaultDataDirectory.cs create mode 100644 BTCPayServer/Configuration/NetworkInformation.cs delete mode 100644 BTCPayServer/Configuration/TextFileConfiguration.cs diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 43842f9de..522e4cbc6 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -77,10 +77,10 @@ namespace BTCPayServer.Tests ServerUri = new Uri("http://127.0.0.1:" + port + "/"); - BTCPayServerOptions options = new BTCPayServerOptions(); - options.LoadArgs(new TextFileConfiguration(new string[] { "-datadir", _Directory })); + var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory }); _Host = new WebHostBuilder() + .UseConfiguration(conf) .ConfigureServices(s => { s.AddSingleton(new MockRateProvider(new Rate("USD", 5000m))); @@ -91,7 +91,6 @@ namespace BTCPayServer.Tests .AddProvider(Logs.LogProvider); }); }) - .AddPayServer(options) .UseKestrel() .UseStartup() .Build(); diff --git a/BTCPayServer.Tests/NBXplorerTester.cs b/BTCPayServer.Tests/NBXplorerTester.cs index e2cc6117d..7a7cfaa50 100644 --- a/BTCPayServer.Tests/NBXplorerTester.cs +++ b/BTCPayServer.Tests/NBXplorerTester.cs @@ -71,7 +71,7 @@ namespace BTCPayServer.Tests config.AppendLine($"rpc.auth={Node.AuthenticationString}"); config.AppendLine($"node.endpoint={Node.NodeEndpoint.Address}:{Node.NodeEndpoint.Port}"); File.WriteAllText(Path.Combine(launcher2.CurrentDirectory, "settings.config"), config.ToString()); - _Process = launcher.Start("dotnet", $"NBXplorer.dll -datadir \"{launcher2.CurrentDirectory}\""); + _Process = launcher.Start("dotnet", $"NBXplorer.dll --datadir \"{launcher2.CurrentDirectory}\""); ExplorerClient = new NBXplorer.ExplorerClient(Node.Network, new Uri($"http://127.0.0.1:{port}/")); CookieFile = Path.Combine(launcher2.CurrentDirectory, ".cookie"); File.Create(CookieFile).Close(); //Will be wipedout when the client starts diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 6cc37989e..c18f15e95 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -15,6 +15,9 @@ + + + diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 30eb07ebf..133e4cccf 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Text; +using StandardConfiguration; +using Microsoft.Extensions.Configuration; namespace BTCPayServer.Configuration { @@ -41,75 +43,20 @@ namespace BTCPayServer.Configuration set; } - public void LoadArgs(TextFileConfiguration consoleConfig) + public void LoadArgs(IConfiguration conf) { - ConfigurationFile = consoleConfig.GetOrDefault("conf", null); - DataDir = consoleConfig.GetOrDefault("datadir", null); - if(DataDir != null && ConfigurationFile != null) - { - var isRelativePath = Path.GetFullPath(ConfigurationFile).Length > ConfigurationFile.Length; - if(isRelativePath) - { - ConfigurationFile = Path.Combine(DataDir, ConfigurationFile); - } - } - - Network = consoleConfig.GetOrDefault("testnet", false) ? Network.TestNet : - consoleConfig.GetOrDefault("regtest", false) ? Network.RegTest : - null; - - if(DataDir != null && ConfigurationFile == null) - { - ConfigurationFile = GetDefaultConfigurationFile(Network != null); - } - - if(ConfigurationFile != null) - { - AssetConfigFileExists(); - var configTemp = TextFileConfiguration.Parse(File.ReadAllText(ConfigurationFile)); - Network = Network ?? (configTemp.GetOrDefault("testnet", false) ? Network.TestNet : - configTemp.GetOrDefault("regtest", false) ? Network.RegTest : - null); - } - - Network = Network ?? Network.Main; - if(DataDir == null) - { - DataDir = DefaultDataDirectory.GetDefaultDirectory("BTCPayServer", Network, true); - ConfigurationFile = GetDefaultConfigurationFile(true); - } - - if(!Directory.Exists(DataDir)) - throw new ConfigurationException("Data directory does not exists"); - - var config = TextFileConfiguration.Parse(File.ReadAllText(ConfigurationFile)); - consoleConfig.MergeInto(config, true); + var networkInfo = DefaultConfiguration.GetNetwork(conf); + Network = networkInfo?.Network; + if(Network == null) + throw new ConfigException("Invalid network"); + DataDir = conf.GetOrDefault("datadir", networkInfo.DefaultDataDirectory); Logs.Configuration.LogInformation("Network: " + Network); - Logs.Configuration.LogInformation("Data directory set to " + DataDir); - Logs.Configuration.LogInformation("Configuration file set to " + ConfigurationFile); - var defaultPort = config.GetOrDefault("port", GetDefaultPort(Network)); - Listen = config - .GetAll("bind") - .Select(p => ConvertToEndpoint(p, defaultPort)) - .ToList(); - if(Listen.Count == 0) - { - Listen.Add(new IPEndPoint(IPAddress.Parse("127.0.0.1"), defaultPort)); - } - - Explorer = config.GetOrDefault("explorer.url", GetDefaultNXplorerUri()); - CookieFile = config.GetOrDefault("explorer.cookiefile", GetExplorerDefaultCookiePath()); - ExternalUrl = config.GetOrDefault("externalurl", null); - if(ExternalUrl == null) - { - var ip = Listen.Where(u => !u.Address.ToString().Equals("0.0.0.0", StringComparison.OrdinalIgnoreCase)).FirstOrDefault() - ?? new IPEndPoint(IPAddress.Parse("127.0.0.1"), defaultPort); - ExternalUrl = new Uri($"http://{ip.Address}:{ip.Port}/"); - } - - RequireHttps = config.GetOrDefault("requirehttps", false); + Explorer = conf.GetOrDefault("explorer.url", networkInfo.DefaultExplorerUrl); + CookieFile = conf.GetOrDefault("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile); + ExternalUrl = conf.GetOrDefault("externalurl", null); + RequireHttps = conf.GetOrDefault("requirehttps", false); } public bool RequireHttps @@ -121,92 +68,5 @@ namespace BTCPayServer.Configuration { get; set; } - - private Uri GetDefaultNXplorerUri() - { - return new Uri("http://localhost:" + GetNXplorerDefaultPort(Network)); - } - - - public string[] GetUrls() - { - return Listen.Select(b => "http://" + b + "/").ToArray(); - } - - private void AssetConfigFileExists() - { - if(!File.Exists(ConfigurationFile)) - throw new ConfigurationException("Configuration file does not exists"); - } - - public static IPEndPoint ConvertToEndpoint(string str, int defaultPort) - { - var portOut = defaultPort; - var hostOut = ""; - int colon = str.LastIndexOf(':'); - // if a : is found, and it either follows a [...], or no other : is in the string, treat it as port separator - bool fHaveColon = colon != -1; - bool fBracketed = fHaveColon && (str[0] == '[' && str[colon - 1] == ']'); // if there is a colon, and in[0]=='[', colon is not 0, so in[colon-1] is safe - bool fMultiColon = fHaveColon && (str.LastIndexOf(':', colon - 1) != -1); - if(fHaveColon && (colon == 0 || fBracketed || !fMultiColon)) - { - int n; - if(int.TryParse(str.Substring(colon + 1), out n) && n > 0 && n < 0x10000) - { - str = str.Substring(0, colon); - portOut = n; - } - } - if(str.Length > 0 && str[0] == '[' && str[str.Length - 1] == ']') - hostOut = str.Substring(1, str.Length - 2); - else - hostOut = str; - return new IPEndPoint(IPAddress.Parse(hostOut), portOut); - } - - const string DefaultConfigFile = "settings.config"; - private string GetDefaultConfigurationFile(bool createIfNotExist) - { - var config = Path.Combine(DataDir, DefaultConfigFile); - Logs.Configuration.LogInformation("Configuration file set to " + config); - if(createIfNotExist && !File.Exists(config)) - { - Logs.Configuration.LogInformation("Creating configuration file"); - StringBuilder builder = new StringBuilder(); - builder.AppendLine("### Global settings ###"); - builder.AppendLine("#testnet=0"); - builder.AppendLine("#regtest=0"); - builder.AppendLine("#Put here the xpub key of your hardware wallet"); - builder.AppendLine("#hdpubkey=xpub..."); - builder.AppendLine(); - builder.AppendLine("### Server settings ###"); - builder.AppendLine("#port=" + GetDefaultPort(Network)); - builder.AppendLine("#bind=127.0.0.1"); - builder.AppendLine("#externalurl=http://127.0.0.1/"); - builder.AppendLine(); - builder.AppendLine("### NBXplorer settings ###"); - builder.AppendLine("#explorer.url=" + GetDefaultNXplorerUri()); - builder.AppendLine("#explorer.cookiefile=" + GetExplorerDefaultCookiePath()); - File.WriteAllText(config, builder.ToString()); - } - return config; - } - - private string GetExplorerDefaultCookiePath() - { - return Path.Combine(DefaultDataDirectory.GetDefaultDirectory("NBXplorer", Network, false), ".cookie"); - } - - private int GetNXplorerDefaultPort(Network network) - { - return network == Network.Main ? 24444 : - network == Network.TestNet ? 24445 : 24446; - } - - private int GetDefaultPort(Network network) - { - return network == Network.Main ? 23000 : - network == Network.TestNet ? 23001 : 23002; - } } } diff --git a/BTCPayServer/Configuration/BTCPayServerRuntime.cs b/BTCPayServer/Configuration/BTCPayServerRuntime.cs index 903b6520f..293e36a36 100644 --- a/BTCPayServer/Configuration/BTCPayServerRuntime.cs +++ b/BTCPayServer/Configuration/BTCPayServerRuntime.cs @@ -47,7 +47,7 @@ namespace BTCPayServer.Configuration } catch(Exception ex) { - throw new ConfigurationException($"Could not connect to NBXplorer, {ex.Message}"); + throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}"); } DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB")); _Resources.Add(db); diff --git a/BTCPayServer/Configuration/ConfigException.cs b/BTCPayServer/Configuration/ConfigException.cs new file mode 100644 index 000000000..116dc2c61 --- /dev/null +++ b/BTCPayServer/Configuration/ConfigException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Configuration +{ + public class ConfigException : Exception + { + public ConfigException(string message) : base(message) + { + + } + } +} diff --git a/BTCPayServer/Configuration/ConfigurationExtensions.cs b/BTCPayServer/Configuration/ConfigurationExtensions.cs new file mode 100644 index 000000000..c509c0c27 --- /dev/null +++ b/BTCPayServer/Configuration/ConfigurationExtensions.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; + +namespace BTCPayServer.Configuration +{ + public static class ConfigurationExtensions + { + public static T GetOrDefault(this IConfiguration configuration, string key, T defaultValue) + { + var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)]; + if(str == null) + return defaultValue; + if(typeof(T) == typeof(bool)) + { + var trueValues = new[] { "1", "true" }; + var falseValues = new[] { "0", "false" }; + if(trueValues.Contains(str, StringComparer.OrdinalIgnoreCase)) + return (T)(object)true; + if(falseValues.Contains(str, StringComparer.OrdinalIgnoreCase)) + return (T)(object)false; + throw new FormatException(); + } + else if(typeof(T) == typeof(Uri)) + return (T)(object)new Uri(str, UriKind.Absolute); + else if(typeof(T) == typeof(string)) + return (T)(object)str; + else if(typeof(T) == typeof(IPEndPoint)) + { + var separator = str.LastIndexOf(":"); + if(separator == -1) + throw new FormatException(); + var ip = str.Substring(0, separator); + var port = str.Substring(separator + 1); + return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port)); + } + else if(typeof(T) == typeof(int)) + { + return (T)(object)int.Parse(str, CultureInfo.InvariantCulture); + } + else + { + throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name); + } + } + } +} diff --git a/BTCPayServer/Configuration/DefaultConfiguration.cs b/BTCPayServer/Configuration/DefaultConfiguration.cs new file mode 100644 index 000000000..e17745120 --- /dev/null +++ b/BTCPayServer/Configuration/DefaultConfiguration.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Threading.Tasks; +using NBitcoin; +using System.Text; +using CommandLine; + +namespace BTCPayServer.Configuration +{ + public class DefaultConfiguration : StandardConfiguration.DefaultConfiguration + { + protected override CommandLineApplication CreateCommandLineApplicationCore() + { + CommandLineApplication app = new CommandLineApplication(true) + { + FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets", + Name = "NBXplorer" + }; + app.HelpOption("-? | -h | --help"); + app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue); + app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue); + app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue); + app.Option("--requirehttps", $"Will redirect to https version of the website (default: false)", CommandOptionType.BoolValue); + app.Option("--externalurl", $"The external url of the website", CommandOptionType.SingleValue); + app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue); + app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue); + + return app; + } + + protected override string GetDefaultDataDir(IConfiguration conf) + { + return GetNetwork(conf).DefaultDataDirectory; + } + + protected override string GetDefaultConfigurationFile(IConfiguration conf) + { + var network = GetNetwork(conf); + var dataDir = conf["datadir"]; + if(dataDir == null) + return network.DefaultConfigurationFile; + var fileName = Path.GetFileName(network.DefaultConfigurationFile); + return Path.Combine(dataDir, fileName); + } + + public static NetworkInformation GetNetwork(IConfiguration conf) + { + var network = conf.GetOrDefault("network", null); + if(network != null) + { + var info = NetworkInformation.GetNetworkByName(network); + if(info == null) + throw new ConfigException($"Invalid network name {network}"); + return info; + } + + var net = conf.GetOrDefault("regtest", false) ? Network.RegTest : + conf.GetOrDefault("testnet", false) ? Network.TestNet : Network.Main; + + return NetworkInformation.GetNetworkByName(net.Name); + } + + protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf) + { + var network = GetNetwork(conf); + StringBuilder builder = new StringBuilder(); + builder.AppendLine("### Global settings ###"); + builder.AppendLine("#testnet=0"); + builder.AppendLine("#regtest=0"); + builder.AppendLine(); + builder.AppendLine("### Server settings ###"); + builder.AppendLine("#requirehttps=0"); + builder.AppendLine("#port=" + network.DefaultPort); + builder.AppendLine("#bind=127.0.0.1"); + builder.AppendLine("#externalurl=http://127.0.0.1/"); + builder.AppendLine(); + builder.AppendLine("### NBXplorer settings ###"); + builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri); + builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile); + return builder.ToString(); + } + + + + protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf) + { + return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort); + } + } +} diff --git a/BTCPayServer/Configuration/DefaultDataDirectory.cs b/BTCPayServer/Configuration/DefaultDataDirectory.cs deleted file mode 100644 index 83fd4c37a..000000000 --- a/BTCPayServer/Configuration/DefaultDataDirectory.cs +++ /dev/null @@ -1,52 +0,0 @@ -using BTCPayServer.Logging; -using Microsoft.Extensions.Logging; -using NBitcoin; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace BTCPayServer.Configuration -{ - public class DefaultDataDirectory - { - public static string GetDefaultDirectory(string appName, Network network, bool createDirectory) - { - string directory = null; - var home = Environment.GetEnvironmentVariable("HOME"); - if(!string.IsNullOrEmpty(home)) - { - if(createDirectory) - Logs.Configuration.LogInformation("Using HOME environment variable for initializing application data"); - directory = home; - directory = Path.Combine(directory, "." + appName.ToLowerInvariant()); - } - else - { - var localAppData = Environment.GetEnvironmentVariable("APPDATA"); - if(!string.IsNullOrEmpty(localAppData)) - { - if(createDirectory) - Logs.Configuration.LogInformation("Using APPDATA environment variable for initializing application data"); - directory = localAppData; - directory = Path.Combine(directory, appName); - } - else - { - throw new DirectoryNotFoundException("Could not find suitable datadir"); - } - } - if(!Directory.Exists(directory) && createDirectory) - { - Directory.CreateDirectory(directory); - } - directory = Path.Combine(directory, network.Name); - if(!Directory.Exists(directory) && createDirectory) - { - Logs.Configuration.LogInformation("Creating data directory"); - Directory.CreateDirectory(directory); - } - return directory; - } - } -} diff --git a/BTCPayServer/Configuration/NetworkInformation.cs b/BTCPayServer/Configuration/NetworkInformation.cs new file mode 100644 index 000000000..13f8bd5e6 --- /dev/null +++ b/BTCPayServer/Configuration/NetworkInformation.cs @@ -0,0 +1,103 @@ +using Microsoft.Extensions.Configuration; +using NBitcoin; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Configuration +{ + public class NetworkInformation + { + static NetworkInformation() + { + _Networks = new Dictionary(); + foreach(var network in Network.GetNetworks()) + { + NetworkInformation info = new NetworkInformation(); + info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name); + info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config"); + info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie"); + info.Network = network; + info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute); + info.DefaultPort = 23002; + _Networks.Add(network.Name, info); + if(network == Network.Main) + { + info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute); + Main = info; + info.DefaultPort = 23000; + } + if(network == Network.TestNet) + { + info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute); + info.DefaultPort = 23001; + } + } + } + + static Dictionary _Networks; + public static NetworkInformation GetNetworkByName(string name) + { + var value = _Networks.TryGet(name); + if(value != null) + return value; + + //Maybe alias ? + var network = Network.GetNetwork(name); + if(network != null) + { + value = _Networks.TryGet(network.Name); + if(value != null) + return value; + } + return null; + } + + public static NetworkInformation Main + { + get; + set; + } + public Network Network + { + get; set; + } + public string DefaultConfigurationFile + { + get; + set; + } + public string DefaultDataDirectory + { + get; + set; + } + public Uri DefaultExplorerUrl + { + get; + internal set; + } + public int DefaultPort + { + get; + private set; + } + public string DefaultExplorerCookieFile + { + get; + internal set; + } + + public override string ToString() + { + return Network.ToString(); + } + + public static string ToStringAll() + { + return string.Join(", ", _Networks.Select(n => n.Key).ToArray()); + } + } +} diff --git a/BTCPayServer/Configuration/TextFileConfiguration.cs b/BTCPayServer/Configuration/TextFileConfiguration.cs deleted file mode 100644 index 939a126fc..000000000 --- a/BTCPayServer/Configuration/TextFileConfiguration.cs +++ /dev/null @@ -1,221 +0,0 @@ -using NBitcoin; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; - -namespace BTCPayServer -{ - public class ConfigurationException : Exception - { - public ConfigurationException(string message) : base(message) - { - - } - } - - public class TextFileConfiguration - { - private Dictionary> _Args; - - public TextFileConfiguration(string[] args) - { - _Args = new Dictionary>(); - string noValueParam = null; - Action flushNoValueParam = () => - { - if(noValueParam != null) - { - Add(noValueParam, "1", false); - noValueParam = null; - } - }; - - foreach(var arg in args) - { - bool isParamName = arg.StartsWith("-", StringComparison.Ordinal); - if(isParamName) - { - var splitted = arg.Split('='); - if(splitted.Length > 1) - { - var value = String.Join("=", splitted.Skip(1).ToArray()); - flushNoValueParam(); - Add(splitted[0], value, false); - } - else - { - flushNoValueParam(); - noValueParam = splitted[0]; - } - } - else - { - if(noValueParam != null) - { - Add(noValueParam, arg, false); - noValueParam = null; - } - } - } - flushNoValueParam(); - } - - private void Add(string key, string value, bool sourcePriority) - { - key = NormalizeKey(key); - List list; - if(!_Args.TryGetValue(key, out list)) - { - list = new List(); - _Args.Add(key, list); - } - if(sourcePriority) - list.Insert(0, value); - else - list.Add(value); - } - - private static string NormalizeKey(string key) - { - key = key.ToLowerInvariant(); - while(key.Length > 0 && key[0] == '-') - { - key = key.Substring(1); - } - key = key.Replace(".", ""); - return key; - } - - public void MergeInto(TextFileConfiguration destination, bool sourcePriority) - { - foreach(var kv in _Args) - { - foreach(var v in kv.Value) - destination.Add(kv.Key, v, sourcePriority); - } - } - - public TextFileConfiguration(Dictionary> args) - { - _Args = args; - } - - public static TextFileConfiguration Parse(string data) - { - Dictionary> result = new Dictionary>(); - var lines = data.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); - int lineCount = -1; - foreach(var l in lines) - { - lineCount++; - var line = l.Trim(); - if(line.StartsWith("#", StringComparison.Ordinal)) - continue; - var split = line.Split('='); - if(split.Length == 0) - continue; - if(split.Length == 1) - throw new FormatException("Line " + lineCount + ": No value are set"); - - var key = split[0]; - key = NormalizeKey(key); - List values; - if(!result.TryGetValue(key, out values)) - { - values = new List(); - result.Add(key, values); - } - var value = String.Join("=", split.Skip(1).ToArray()); - values.Add(value); - } - return new TextFileConfiguration(result); - } - - public bool Contains(string key) - { - List values; - return _Args.TryGetValue(key, out values); - } - public string[] GetAll(string key) - { - List values; - if(!_Args.TryGetValue(key, out values)) - return new string[0]; - return values.ToArray(); - } - - private List> _Aliases = new List>(); - - public void AddAlias(string from, string to) - { - from = NormalizeKey(from); - to = NormalizeKey(to); - _Aliases.Add(Tuple.Create(from, to)); - } - public T GetOrDefault(string key, T defaultValue) - { - key = NormalizeKey(key); - - var aliases = _Aliases - .Where(a => a.Item1 == key || a.Item2 == key) - .Select(a => a.Item1 == key ? a.Item2 : a.Item1) - .ToList(); - aliases.Insert(0, key); - - foreach(var alias in aliases) - { - List values; - if(!_Args.TryGetValue(alias, out values)) - continue; - if(values.Count == 0) - continue; - try - { - return ConvertValue(values[0]); - } - catch(FormatException) { throw new ConfigurationException("Key " + key + " should be of type " + typeof(T).Name); } - } - return defaultValue; - } - - private T ConvertValue(string str) - { - if(typeof(T) == typeof(bool)) - { - var trueValues = new[] { "1", "true" }; - var falseValues = new[] { "0", "false" }; - if(trueValues.Contains(str, StringComparer.OrdinalIgnoreCase)) - return (T)(object)true; - if(falseValues.Contains(str, StringComparer.OrdinalIgnoreCase)) - return (T)(object)false; - throw new FormatException(); - } - else if(typeof(T) == typeof(Uri)) - return (T)(object)new Uri(str, UriKind.Absolute); - else if(typeof(T) == typeof(string)) - return (T)(object)str; - else if(typeof(T) == typeof(IPEndPoint)) - { - var separator = str.LastIndexOf(":"); - if(separator == -1) - throw new FormatException(); - var ip = str.Substring(0, separator); - var port = str.Substring(separator + 1); - return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port)); - } - else if(typeof(T) == typeof(int)) - { - return (T)(object)int.Parse(str, CultureInfo.InvariantCulture); - } - else - { - throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name); - } - } - } -} diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 065d992b0..2c1dcf649 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -1,5 +1,8 @@ using BTCPayServer.Authentication; +using BTCPayServer.Configuration; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Text; @@ -8,6 +11,17 @@ namespace BTCPayServer { public static class Extensions { + + public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf) + { + services.Configure(o => + { + o.LoadArgs(conf); + }); + return services; + } + + public static BitIdentity GetBitIdentity(this Controller controller) { if(!(controller.User.Identity is BitIdentity)) diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index d0e33ed1c..377c055f3 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -22,70 +22,142 @@ using BTCPayServer.Services.Stores; using BTCPayServer.Services.Fees; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Rewrite; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Authorization; +using BTCPayServer.Controllers; +using BTCPayServer.Services.Mails; +using Microsoft.AspNetCore.Identity; +using BTCPayServer.Models; +using System.Threading.Tasks; namespace BTCPayServer.Hosting { public static class BTCPayServerServices { - public static IWebHostBuilder AddPayServer(this IWebHostBuilder builder, BTCPayServerOptions options) + public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement { - return - builder - .ConfigureServices(c => + public OwnStoreAuthorizationRequirement() + { + } + + public OwnStoreAuthorizationRequirement(string role) + { + Role = role; + } + + public string Role + { + get; set; + } + } + + public class OwnStoreHandler : AuthorizationHandler + { + StoreRepository _StoreRepository; + UserManager _UserManager; + public OwnStoreHandler(StoreRepository storeRepository, UserManager userManager) + { + _StoreRepository = storeRepository; + _UserManager = userManager; + } + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement) + { + object storeId = null; + if(!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId)) + context.Succeed(requirement); + else { - c.AddDbContext(o => - { - var path = Path.Combine(options.DataDir, "sqllite.db"); - o.UseSqlite("Data Source=" + path); - }); - c.AddSingleton(options); - c.AddSingleton(o => - { - var runtime = new BTCPayServerRuntime(); - runtime.Configure(options); - return runtime; - }); + var store = await _StoreRepository.FindStore((string)storeId, _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User)); + if(store != null) + if(requirement.Role == null || requirement.Role == store.Role) + context.Succeed(requirement); + } + } + } + class BTCPayServerConfigureOptions : IConfigureOptions + { + BTCPayServerOptions _Options; + public BTCPayServerConfigureOptions(BTCPayServerOptions options) + { + _Options = options; + } + public void Configure(MvcOptions options) + { + if(_Options.RequireHttps) + options.Filters.Add(new RequireHttpsAttribute()); + } + } + public static IServiceCollection AddBTCPayServer(this IServiceCollection services) + { + services.AddDbContext((provider, o) => + { + var path = Path.Combine(provider.GetRequiredService().DataDir, "sqllite.db"); + o.UseSqlite("Data Source=" + path); + }); + services.TryAddSingleton(o => o.GetRequiredService>().Value); + services.TryAddSingleton, BTCPayServerConfigureOptions>(); + services.TryAddSingleton(o => + { + var runtime = new BTCPayServerRuntime(); + runtime.Configure(o.GetRequiredService()); + return runtime; + }); + services.TryAddSingleton(o => o.GetRequiredService().TokenRepository); + services.TryAddSingleton(o => o.GetRequiredService().InvoiceRepository); + services.TryAddSingleton(o => o.GetRequiredService().Network); + services.TryAddSingleton(o => o.GetRequiredService().DBFactory); + services.TryAddSingleton(); + services.TryAddSingleton(o => o.GetRequiredService().Wallet); + services.TryAddSingleton(); + services.TryAddSingleton(o => new NBXplorerFeeProvider() + { + Fallback = new FeeRate(100, 1), + BlockTarget = 20, + ExplorerClient = o.GetRequiredService() + }); + services.TryAddSingleton(o => + { + var runtime = o.GetRequiredService(); + return runtime.Explorer; + }); + services.TryAddSingleton(o => + { + if(o.GetRequiredService().Network == Network.Main) + return new Bitpay(new Key(), new Uri("https://bitpay.com/")); + else + return new Bitpay(new Key(), new Uri("https://test.bitpay.com/")); + }); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(o => o.GetRequiredService()); + services.TryAddScoped(); + services.TryAddSingleton(o => + { + var op = o.GetRequiredService(); + if(op.ExternalUrl != null) + return new FixedExternalUrlProvider(op.ExternalUrl, o.GetRequiredService()); + return new DefaultExternalUrlProvider(o.GetRequiredService()); + }); + services.TryAddSingleton(); + services.AddTransient(); + // Add application services. + services.AddTransient(); - if(options.RequireHttps) - { - c.Configure(o => - { - o.Filters.Add(new RequireHttpsAttribute()); - }); - } + services.AddAuthorization(o => + { + o.AddPolicy("CanAccessStore", builder => + { + builder.AddRequirements(new OwnStoreAuthorizationRequirement()); + }); - c.AddSingleton(options.Network); - c.AddSingleton(o => o.GetRequiredService().TokenRepository); - c.AddSingleton(o => o.GetRequiredService().InvoiceRepository); - c.AddSingleton(o => o.GetRequiredService().DBFactory); - c.AddSingleton(); - c.AddSingleton(o => o.GetRequiredService().Wallet); - c.AddSingleton(); - c.AddSingleton(o => new NBXplorerFeeProvider() - { - Fallback = new FeeRate(100, 1), - BlockTarget = 20, - ExplorerClient = o.GetRequiredService() - }); - c.AddSingleton(o => - { - var runtime = o.GetRequiredService(); - return runtime.Explorer; - }); - c.AddSingleton(o => - { - if(options.Network == Network.Main) - return new Bitpay(new Key(), new Uri("https://bitpay.com/")); - else - return new Bitpay(new Key(), new Uri("https://test.bitpay.com/")); - }); - c.TryAddSingleton(); - c.AddSingleton(); - c.AddSingleton(o => o.GetRequiredService()); - c.AddScoped(); - c.AddSingleton(o => new FixedExternalUrlProvider(options.ExternalUrl, o.GetRequiredService())); - }) - .UseUrls(options.GetUrls()); + o.AddPolicy("OwnStore", builder => + { + builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner")); + }); + }); + + return services; } public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 115825198..b65d8ca50 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -23,45 +23,30 @@ using System.Threading.Tasks; using BTCPayServer.Controllers; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Mails; +using Microsoft.Extensions.Configuration; namespace BTCPayServer.Hosting { public class Startup { + public Startup(IConfiguration conf) + { + Configuration = conf; + } + + public IConfiguration Configuration + { + get; set; + } public void ConfigureServices(IServiceCollection services) { + services.ConfigureBTCPayServer(Configuration); services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - services.AddAuthorization(o => - { - o.AddPolicy("CanAccessStore", builder => - { - builder.AddRequirements(new OwnStoreAuthorizationRequirement()); - }); - - o.AddPolicy("OwnStore", builder => - { - builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner")); - }); - }); - services.AddSingleton(); - services.AddTransient(); - // Add application services. - services.AddTransient(); - - //services.AddSingleton(); - services.AddMvcCore(o => - { - //o.Filters.Add(new NBXplorerExceptionFilter()); - o.OutputFormatters.Clear(); - o.InputFormatters.Clear(); - }) - .AddJsonFormatters() - .AddFormatterMappings(); - + services.AddBTCPayServer(); services.AddMvc(); } public void Configure( @@ -74,6 +59,8 @@ namespace BTCPayServer.Hosting app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } + + Logs.Configure(loggerFactory); app.UsePayServer(); app.UseStaticFiles(); @@ -87,45 +74,4 @@ namespace BTCPayServer.Hosting }); } } - - public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement - { - public OwnStoreAuthorizationRequirement() - { - } - - public OwnStoreAuthorizationRequirement(string role) - { - Role = role; - } - - public string Role - { - get; set; - } - } - - public class OwnStoreHandler : AuthorizationHandler - { - StoreRepository _StoreRepository; - UserManager _UserManager; - public OwnStoreHandler(StoreRepository storeRepository, UserManager userManager) - { - _StoreRepository = storeRepository; - _UserManager = userManager; - } - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement) - { - object storeId = null; - if(!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId)) - context.Succeed(requirement); - else - { - var store = await _StoreRepository.FindStore((string)storeId, _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User)); - if(store != null) - if(requirement.Role == null || requirement.Role == store.Role) - context.Succeed(requirement); - } - } - } } diff --git a/BTCPayServer/Program.cs b/BTCPayServer/Program.cs index c47aceb56..9309865fd 100644 --- a/BTCPayServer/Program.cs +++ b/BTCPayServer/Program.cs @@ -13,6 +13,8 @@ using System.IO; using System.Net; using System.Collections.Generic; using System.Collections; +using Microsoft.AspNetCore.Hosting.Server.Features; +using System.Threading; namespace BTCPayServer { @@ -22,18 +24,22 @@ namespace BTCPayServer { ServicePointManager.DefaultConnectionLimit = 100; IWebHost host = null; + CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider(); + + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(loggerProvider); + var logger = loggerFactory.CreateLogger("Configuration"); try { - var conf = new BTCPayServerOptions(); - var arguments = new TextFileConfiguration(args); - arguments = LoadEnvironmentVariables(arguments); - conf.LoadArgs(arguments); + var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args); + if(conf == null) + return; host = new WebHostBuilder() - .AddPayServer(conf) .UseKestrel() .UseIISIntegration() .UseContentRoot(Directory.GetCurrentDirectory()) + .UseConfiguration(conf) .ConfigureServices(services => { services.AddLogging(l => @@ -44,11 +50,19 @@ namespace BTCPayServer }) .UseStartup() .Build(); - var running = host.RunAsync(); - OpenBrowser(conf.GetUrls().Select(url => url.Replace("0.0.0.0", "127.0.0.1")).First()); - running.GetAwaiter().GetResult(); - } - catch(ConfigurationException ex) + host.StartAsync().GetAwaiter().GetResult(); + var urls = host.ServerFeatures.Get().Addresses; + if(urls.Count != 0) + { + OpenBrowser(urls.Select(url => url.Replace("0.0.0.0", "127.0.0.1")).First()); + } + foreach(var url in urls) + { + logger.LogInformation("Listening on " + url); + } + host.WaitForShutdown(); + } + catch(ConfigException ex) { if(!string.IsNullOrEmpty(ex.Message)) Logs.Configuration.LogError(ex.Message); @@ -62,30 +76,10 @@ namespace BTCPayServer { if(host != null) host.Dispose(); + loggerProvider.Dispose(); } } - private static TextFileConfiguration LoadEnvironmentVariables(TextFileConfiguration args) - { - var variables = Environment.GetEnvironmentVariables(); - List values = new List(); - foreach(DictionaryEntry variable in variables) - { - var key = (string)variable.Key; - var value = (string)variable.Value; - if(key.StartsWith("APPSETTING_", StringComparison.Ordinal)) - { - key = key.Substring("APPSETTING_".Length); - values.Add("-" + key); - values.Add(value); - } - } - - TextFileConfiguration envConfig = new TextFileConfiguration(values.ToArray()); - args.MergeInto(envConfig, true); - return envConfig; - } - public static void OpenBrowser(string url) { try diff --git a/BTCPayServer/wwwroot/img/ibuki.png b/BTCPayServer/wwwroot/img/ibuki.png index 6bc6b1e794a30da5d45a40a7223e24a873daa06f..8c85da96d77b2218e39d8317abca824900977c4c 100644 GIT binary patch literal 1431 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

Nk!1Vrnl^`oL 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