using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using Oqtane.Infrastructure; using Oqtane.Modules; using Oqtane.Repository; using Oqtane.Security; using Oqtane.Services; using Oqtane.Shared; namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { public static IServiceCollection AddOqtane(this IServiceCollection services, string[] supportedCultures) { LoadAssemblies(); LoadSatelliteAssemblies(supportedCultures); services.AddOqtaneServices(); return services; } public static IServiceCollection AddOqtaneDbContext(this IServiceCollection services) { services.AddDbContext(options => { }, ServiceLifetime.Transient); services.AddDbContext(options => { }, ServiceLifetime.Transient); return services; } public static IServiceCollection AddOqtaneAuthorizationPolicies(this IServiceCollection services) { services.AddAuthorizationCore(options => { options.AddPolicy(PolicyNames.ViewPage, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Page, PermissionNames.View))); options.AddPolicy(PolicyNames.EditPage, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Page, PermissionNames.Edit))); options.AddPolicy(PolicyNames.ViewModule, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Module, PermissionNames.View))); options.AddPolicy(PolicyNames.EditModule, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Module, PermissionNames.Edit))); options.AddPolicy(PolicyNames.ViewFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.View))); options.AddPolicy(PolicyNames.EditFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.Edit))); options.AddPolicy(PolicyNames.ListFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.Browse))); }); return services; } public static OqtaneSiteOptionsBuilder AddOqtaneSiteOptions(this IServiceCollection services) { return new OqtaneSiteOptionsBuilder(services); } internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); return services; } internal static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services) { services.AddScoped(); return services; } internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) { services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // obsolete - replaced by ITenantManager services.AddTransient(); return services; } public static IServiceCollection ConfigureOqtaneCookieOptions(this IServiceCollection services) { services.ConfigureApplicationCookie(options => { options.Cookie.HttpOnly = false; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return Task.CompletedTask; }; options.Events.OnRedirectToAccessDenied = context => { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return Task.CompletedTask; }; options.Events.OnRedirectToLogout = context => { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return Task.CompletedTask; }; options.Events.OnValidatePrincipal = PrincipalValidator.ValidateAsync; }); return services; } public static IServiceCollection ConfigureOqtaneAuthenticationOptions(this IServiceCollection services, IConfigurationRoot Configuration) { // settings defined in appsettings services.Configure(Configuration); services.Configure(Configuration); return services; } public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services, IConfigurationRoot Configuration) { // default settings services.Configure(options => { // Password settings options.Password.RequireDigit = true; options.Password.RequiredLength = 6; options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true; options.Password.RequiredUniqueChars = 1; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = false; // SignIn settings options.SignIn.RequireConfirmedEmail = true; options.SignIn.RequireConfirmedPhoneNumber = false; // User settings options.User.RequireUniqueEmail = false; // changing to true will cause issues for legacy data options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; }); // overrides defined in appsettings services.Configure(Configuration); return services; } internal static IServiceCollection AddHttpClients(this IServiceCollection services) { if (!services.Any(x => x.ServiceType == typeof(HttpClient))) { services.AddScoped(s => { // creating the URI helper needs to wait until the JS Runtime is initialized, so defer it. var navigationManager = s.GetRequiredService(); var client = new HttpClient(new HttpClientHandler { UseCookies = false }); client.BaseAddress = new Uri(navigationManager.Uri); // set the cookies to allow HttpClient API calls to be authenticated var httpContextAccessor = s.GetRequiredService(); foreach (var cookie in httpContextAccessor.HttpContext.Request.Cookies) { client.DefaultRequestHeaders.Add("Cookie", cookie.Key + "=" + cookie.Value); } return client; }); } // IHttpClientFactory for calling remote services via RemoteServiceBase services.AddHttpClient(); return services; } internal static IServiceCollection TryAddSwagger(this IServiceCollection services, bool useSwagger) { if (useSwagger) { services.AddSwaggerGen(c => { c.SwaggerDoc(Constants.Version, new OpenApiInfo { Title = Constants.PackageId, Version = Constants.Version }); }); } return services; } private static IServiceCollection AddOqtaneServices(this IServiceCollection services) { if (services is null) { throw new ArgumentNullException(nameof(services)); } var hostedServiceType = typeof(IHostedService); var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); foreach (var assembly in assemblies) { // dynamically register module services, contexts, and repository classes var implementationTypes = assembly.GetInterfaces(); foreach (var implementationType in implementationTypes) { if (implementationType.AssemblyQualifiedName != null) { var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); services.AddScoped(serviceType ?? implementationType, implementationType); } } // dynamically register hosted services var serviceTypes = assembly.GetTypes(hostedServiceType); foreach (var serviceType in serviceTypes) { if (serviceType.IsSubclassOf(typeof(HostedServiceBase))) { services.AddSingleton(hostedServiceType, serviceType); } } // dynamically register server startup services assembly.GetInstances() .ToList() .ForEach(x => x.ConfigureServices(services)); // dynamically register client startup services (these services will only be used when running on Blazor Server) assembly.GetInstances() .ToList() .ForEach(x => x.ConfigureServices(services)); } return services; } private static void LoadAssemblies() { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); if (assemblyPath == null) return; AssemblyLoadContext.Default.Resolving += ResolveDependencies; var assembliesFolder = new DirectoryInfo(assemblyPath); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); // iterate through Oqtane assemblies in /bin ( filter is narrow to optimize loading process ) foreach (var dll in assembliesFolder.EnumerateFiles($"*.dll", SearchOption.TopDirectoryOnly).Where(f => f.IsOqtaneAssembly())) { AssemblyName assemblyName; try { assemblyName = AssemblyName.GetAssemblyName(dll.FullName); } catch { Debug.WriteLine($"Oqtane Error: Cannot Get Assembly Name For {dll.Name}"); continue; } if (!assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.GetName()))) { AssemblyLoadContext.Default.LoadOqtaneAssembly(dll, assemblyName); } } } private static void LoadSatelliteAssemblies(string[] supportedCultures) { var assemblies = AppDomain.CurrentDomain.GetAssemblies(); var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); if (assemblyPath == null) { return; } AssemblyLoadContext.Default.Resolving += ResolveDependencies; foreach (var culture in supportedCultures) { if (culture == Constants.DefaultCulture) { continue; } var assembliesFolder = new DirectoryInfo(Path.Combine(assemblyPath, culture)); if (assembliesFolder.Exists) { foreach (var assemblyFile in assembliesFolder.EnumerateFiles($"*{Constants.SatelliteAssemblyExtension}")) { AssemblyName assemblyName; try { assemblyName = AssemblyName.GetAssemblyName(assemblyFile.FullName); } catch { Debug.WriteLine($"Oqtane Error: Cannot Get Satellite Assembly Name For {assemblyFile.Name}"); continue; } try { Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyFile.FullName))); Debug.WriteLine($"Oqtane Info: Loaded Assembly {assemblyName}"); } catch (Exception ex) { Debug.WriteLine($"Oqtane Error: Unable To Load Assembly {assemblyName} - {ex}"); } } } else { Debug.WriteLine($"Oqtane Error: The Satellite Assembly Folder For {culture} Does Not Exist"); } } } private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name) { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + Path.DirectorySeparatorChar + name.Name + ".dll"; if (System.IO.File.Exists(assemblyPath)) { return context.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyPath))); } else { return null; } } } }