using BTCPayServer.Configuration; using BTCPayServer.Services.Altcoins.Monero; using Microsoft.Extensions.Logging; using System; using System.IdentityModel.Tokens.Jwt; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.AspNetCore.Http; using NBitpayClient; using NBitcoin; using BTCPayServer.Data; using Microsoft.EntityFrameworkCore; using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Fees; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using BTCPayServer.Controllers; using BTCPayServer.Services.Mails; using System.Threading; using BTCPayServer.Services.Wallets; using BTCPayServer.Authentication; using BTCPayServer.Logging; using BTCPayServer.HostedServices; using BTCPayServer.PaymentRequest; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.Lightning; using BTCPayServer.Security; using BTCPayServer.Services.PaymentRequests; using Microsoft.AspNetCore.Mvc.ModelBinding; using NBXplorer.DerivationStrategy; using NicolasDorier.RateLimits; using Npgsql; using BTCPayServer.Services.Apps; using BTCPayServer.U2F; using BundlerMinifier.TagHelpers; using OpenIddict.EntityFrameworkCore.Models; using System.Collections.Generic; using System.Diagnostics; using System.Security.Claims; using System.Threading.Tasks; using BTCPayServer.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Routing; namespace BTCPayServer.Hosting { public static class BTCPayServerServices { public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration) { #if NETCOREAPP21 services.AddSingleton(); #else services.AddSingleton(o => o.GetRequiredService>().Value); #endif services.AddDbContext((provider, o) => { var factory = provider.GetRequiredService(); factory.ConfigureBuilder(o); o.UseOpenIddict, BTCPayOpenIdToken, string>(); }); services.AddHttpClient(); services.AddHttpClient(nameof(ExplorerClientProvider), httpClient => { httpClient.Timeout = Timeout.InfiniteTimeSpan; }); services.AddMoneroLike(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(o => o.GetRequiredService>().Value); services.AddStartupTask(); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); var dbContext = o.GetRequiredService(); var dbpath = Path.Combine(opts.DataDir, "InvoiceDB"); if (!Directory.Exists(dbpath)) Directory.CreateDirectory(dbpath); return new InvoiceRepository(dbContext, dbpath, o.GetRequiredService()); }); services.AddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); ApplicationDbContextFactory dbContext = null; if (!String.IsNullOrEmpty(opts.PostgresConnectionString)) { Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})"); dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString); } else if(!String.IsNullOrEmpty(opts.MySQLConnectionString)) { Logs.Configuration.LogInformation($"MySQL DB used ({opts.MySQLConnectionString})"); Logs.Configuration.LogWarning("MySQL is not widely tested and should be considered experimental, we advise you to use postgres instead."); dbContext = new ApplicationDbContextFactory(DatabaseType.MySQL, opts.MySQLConnectionString); } else { var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db"); Logs.Configuration.LogInformation($"SQLite DB used ({connStr})"); Logs.Configuration.LogWarning("MySQL is not widely tested and should be considered experimental, we advise you to use postgres instead."); dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr); } return dbContext; }); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); return opts.NetworkProvider; }); services.TryAddSingleton(); services.TryAddTransient(); services.TryAddSingleton(o => { var htmlSanitizer = new Ganss.XSS.HtmlSanitizer(); htmlSanitizer.RemovingAtRule += (sender, args) => { }; htmlSanitizer.RemovingTag += (sender, args) => { if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase)) { if (!args.Tag.ClassList.Contains("img-fluid")) { args.Tag.ClassList.Add("img-fluid"); } args.Cancel = true; } }; htmlSanitizer.RemovingAttribute += (sender, args) => { if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase) && args.Attribute.Name.Equals("src", StringComparison.InvariantCultureIgnoreCase) && args.Reason == Ganss.XSS.RemoveReason.NotAllowedUrlValue) { args.Cancel = true; } }; htmlSanitizer.RemovingStyle += (sender, args) => { args.Cancel = true; }; htmlSanitizer.AllowedAttributes.Add("class"); htmlSanitizer.AllowedTags.Add("iframe"); htmlSanitizer.AllowedTags.Remove("img"); htmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen"); htmlSanitizer.AllowedAttributes.Add("allowfullscreen"); return htmlSanitizer; }); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(o => new NBXplorerFeeProviderFactory(o.GetRequiredService()) { Fallback = new FeeRate(100L, 1), BlockTarget = 20 }); services.AddSingleton(); services.Configure((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(WalletId))); o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase))); }); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); services.AddSingleton(); services.AddSingleton(provider => provider.GetService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(provider => provider.GetService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddTransient, BTCPayClaimsFilter>(); services.TryAddSingleton(); services.TryAddSingleton(o => { if (o.GetRequiredService().NetworkType == NetworkType.Mainnet) 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.TryAddScoped(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // Add application services. services.AddSingleton(); // bundling services.AddAuthorization(o => o.AddBTCPayPolicies().AddBTCPayRESTApiPolicies()); services.AddBtcPayServerAuthenticationSchemes(configuration); services.AddSingleton(); services.AddTransient(provider => { var opts = provider.GetRequiredService(); var bundle = new BundleOptions(); bundle.UseBundles = opts.BundleJsCss; bundle.AppendVersion = true; return bundle; }); services.AddCors(options => { options.AddPolicy(CorsPolicies.All, p => p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()); }); var rateLimits = new RateLimitService(); rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay"); services.AddSingleton(rateLimits); return services; } private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services, IConfiguration configuration) { JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear(); services.AddAuthentication() .AddJwtBearer(options => { //Disabled so that Tor works witt JWT auth options.RequireHttpsMetadata = false; options.TokenValidationParameters.ValidateAudience = false; //we do not validate the issuer directly because btcpay can be accessed through multiple urls that we cannot predetermine options.TokenValidationParameters.ValidateIssuer = false; options.TokenValidationParameters.IssuerSigningKey = OpenIddictExtensions.GetSigningKey(configuration); options.IncludeErrorDetails = true; options.Events = new JwtBearerEvents() { OnTokenValidated = async context => { var routeData = context.HttpContext.GetRouteData(); var identity = ((ClaimsIdentity)context.Principal.Identity); if (context.Principal.IsInRole(Roles.ServerAdmin)) { identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true")); } if (context.HttpContext.GetStoreData() != null || !routeData.Values.TryGetValue("storeId", out var storeId)) { return; } var userManager = context.HttpContext.RequestServices .GetService>(); var storeRepository = context.HttpContext.RequestServices .GetService(); var userid = userManager.GetUserId(context.Principal); if (!string.IsNullOrEmpty(userid)) { var store = await storeRepository.FindStore((string)storeId, userid); if (store == null) { context.Fail("Could not authorize you against store access"); } else { context.HttpContext.SetStoreData(store); identity.AddClaims(store.GetClaims()); } } } }; }) .AddCookie() .AddBitpayAuthentication(); } public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) { app.UseMiddleware(); return app; } public static IApplicationBuilder UseHeadersOverride(this IApplicationBuilder app) { app.UseMiddleware(); return app; } } }