using System.Reflection; using System.Text.Json; using Scalar.AspNetCore; using MyCompany.MyProject.BackendApi.Models; using System.Security; // Ensure S8n.Components.Packages assembly is loaded _ = typeof(S8n.Components.Basics.Calculator).Assembly; var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(); } app.UseHttpsRedirection(); // Component registry endpoint app.MapGet("/api/components", () => { var components = new List(); var presets = new List(); // Load components from components/ directory var componentsPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "components"); if (!Directory.Exists(componentsPath)) { // Fallback to absolute path from workspace root componentsPath = Path.Combine(Directory.GetCurrentDirectory(), "components"); } // Load presets from components_user/ directory var userComponentsPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "components_user"); if (!Directory.Exists(userComponentsPath)) { userComponentsPath = Path.Combine(Directory.GetCurrentDirectory(), "components_user"); } // Load all JSON files from both directories var allPaths = new List(); if (Directory.Exists(componentsPath)) allPaths.Add(componentsPath); if (Directory.Exists(userComponentsPath)) allPaths.Add(userComponentsPath); foreach (var path in allPaths) { var jsonFiles = Directory.GetFiles(path, "*.json", SearchOption.AllDirectories); foreach (var file in jsonFiles) { try { var json = File.ReadAllText(file); // Try to determine if this is a component or preset by checking for "type" field using var doc = JsonDocument.Parse(json); var isPreset = doc.RootElement.TryGetProperty("type", out var typeElement) && typeElement.GetString() == "preset"; if (isPreset) { var preset = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (preset != null) { presets.Add(preset); } } else { var component = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (component != null) { components.Add(component); } } } catch (Exception ex) { // Log error but continue Console.WriteLine($"Error reading component/preset file {file}: {ex.Message}"); } } } // Group presets by baseComponentCode and attach to components var presetsByComponent = presets.GroupBy(p => p.BaseComponentCode) .ToDictionary(g => g.Key, g => g.ToList()); foreach (var component in components) { if (presetsByComponent.TryGetValue(component.Code, out var componentPresets)) { component.Presets = componentPresets; } } return Results.Ok(components); }) .WithName("GetComponents"); // Runtime execution endpoint app.MapPost("/api/runtime/execute", async (RuntimeRequest request, ILogger logger) => { const int MAX_CLASS_NAME_LENGTH = 500; const int MAX_METHOD_NAME_LENGTH = 100; try { // Input validation and sanitization if (string.IsNullOrWhiteSpace(request.ClassName) || string.IsNullOrWhiteSpace(request.MethodName) || request.ClassName.Length > MAX_CLASS_NAME_LENGTH || request.MethodName.Length > MAX_METHOD_NAME_LENGTH) { logger.LogWarning("Invalid input parameters received"); return Results.BadRequest(new RuntimeResponse { Error = "Invalid class name or method name" }); } // Security: Validate class name format to prevent malicious type loading if (!IsValidTypeName(request.ClassName)) { logger.LogWarning("Invalid type name format: {ClassName}", request.ClassName); return Results.BadRequest(new RuntimeResponse { Error = "Invalid class name format" }); } // Security: Validate method name format if (!IsValidMethodName(request.MethodName)) { logger.LogWarning("Invalid method name format: {MethodName}", request.MethodName); return Results.BadRequest(new RuntimeResponse { Error = "Invalid method name format" }); } // Get the type from the class name with security restrictions var type = GetTypeSafely(request.ClassName); if (type == null) { logger.LogWarning("Class not found: {ClassName}", request.ClassName); return Results.NotFound(new RuntimeResponse { Error = $"Class not found: {request.ClassName}" }); } // Security: Verify the type is in allowed assemblies/namespaces if (!IsAllowedAssembly(type.Assembly)) { logger.LogWarning("Access to type from disallowed assembly: {Assembly}", type.Assembly.FullName); return Results.Forbid(); } // Security: Check if type is instantiable and safe if (!IsSafeInstantiableType(type)) { logger.LogWarning("Attempted to instantiate unsafe type: {Type}", type.FullName); return Results.BadRequest(new RuntimeResponse { Error = "Type instantiation is not allowed" }); } // Create instance with dependency injection if available var instance = CreateInstanceSafely(type); if (instance == null) { logger.LogError("Failed to create instance of: {ClassName}", request.ClassName); return Results.BadRequest(new RuntimeResponse { Error = $"Failed to create instance of: {request.ClassName}" }); } // Get the method with security checks var method = type.GetMethod(request.MethodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); if (method == null) { logger.LogWarning("Method not found: {MethodName} in type {Type}", request.MethodName, type.FullName); return Results.NotFound(new RuntimeResponse { Error = $"Method not found: {request.MethodName}" }); } // Security: Verify method is safe to execute if (!IsSafeExecutableMethod(method)) { logger.LogWarning("Unsafe method attempted: {Method} in type {Type}", method.Name, type.FullName); return Results.BadRequest(new RuntimeResponse { Error = "Method execution is not allowed" }); } // Execute method with proper parameter binding var result = await ExecuteMethodSafely(instance, method, request.Inputs, logger); return Results.Ok(new RuntimeResponse { Outputs = result }); } catch (TargetInvocationException tie) when (tie.InnerException != null) { logger.LogError(tie.InnerException, "Method execution failed for {ClassName}.{MethodName}", request?.ClassName, request?.MethodName); return Results.BadRequest(new RuntimeResponse { Error = tie.InnerException.Message }); } catch (SecurityException se) { logger.LogError(se, "Security violation during execution of {ClassName}.{MethodName}", request?.ClassName, request?.MethodName); return Results.Forbid(); } catch (Exception ex) { logger.LogError(ex, "Unexpected error during runtime execution for {ClassName}.{MethodName}", request?.ClassName, request?.MethodName); return Results.Problem("An unexpected error occurred during execution", statusCode: 500); } }) .WithName("ExecuteRuntime"); // Helper methods for enhanced security and functionality static bool IsValidTypeName(string typeName) { // Basic validation - only allow alphanumeric characters, dots, underscores, and spaces return System.Text.RegularExpressions.Regex.IsMatch(typeName, @"^[a-zA-Z0-9._]+$") && !typeName.Contains("..") && // Prevent path traversal attempts !typeName.StartsWith(".") && // Prevent relative paths !typeName.EndsWith("."); // Prevent malformed names } static bool IsValidMethodName(string methodName) { // Basic validation for method names return System.Text.RegularExpressions.Regex.IsMatch(methodName, @"^[a-zA-Z_][a-zA-Z0-9_]*$"); } static Type? GetTypeSafely(string className) { // First try direct lookup var type = Type.GetType(className); if (type != null) return type; // Search in loaded assemblies with additional safety var assemblies = AppDomain.CurrentDomain.GetAssemblies() .Where(a => !a.IsDynamic && !a.FullName!.StartsWith("System.Private")) .Take(50); // Limit search scope for performance foreach (var assembly in assemblies) { type = assembly.GetType(className); if (type != null) return type; } return null; } static bool IsAllowedAssembly(Assembly assembly) { var assemblyName = assembly.GetName().Name; // Allow only specific assemblies (customize based on your needs) var allowedPrefixes = new[] { "S8n.Components.", // Replace with your application namespace "MyCompany.MyProject." // Replace with your library namespaces }; return allowedPrefixes.Any(prefix => assemblyName!.StartsWith(prefix)); } static bool IsSafeInstantiableType(Type type) { // Don't allow instantiation of system types, delegates, interfaces, abstract classes return !type.IsInterface && !type.IsAbstract && !type.IsGenericTypeDefinition && !type.IsNotPublic && !type.Namespace!.StartsWith("System.") && // More restrictive than just containing "System" !type.Namespace.StartsWith("Microsoft."); } static object? CreateInstanceSafely(Type type) { try { // Use Activator.CreateInstance with proper exception handling return Activator.CreateInstance(type, nonPublic: false); } catch { return null; } } static bool IsSafeExecutableMethod(MethodInfo method) { // Don't allow access to potentially dangerous methods var dangerousNames = new[] { "GetType", "MemberwiseClone", "Finalize", "Dispose" }; if (dangerousNames.Contains(method.Name, StringComparer.OrdinalIgnoreCase)) return false; // Check if method has dangerous attributes or return types var returnType = method.ReturnType; if (returnType == typeof(Type)) return false; // Potential information disclosure return true; } static async Task ExecuteMethodSafely(object instance, MethodInfo method, object? inputs, ILogger logger) { var parameters = method.GetParameters(); object?[] args; if (inputs is JsonElement jsonElement) { args = BindParameters(parameters, jsonElement, logger); } else { args = []; } var result = method.Invoke(instance, args); // Handle async methods properly if (result is Task taskResult) { await taskResult.ConfigureAwait(false); // Extract result from generic Task if (taskResult.GetType().IsGenericType) { var resultProperty = taskResult.GetType().GetProperty("Result"); if (resultProperty != null) { return resultProperty.GetValue(taskResult); } } return null; // For non-generic Task } return result; } static object?[] BindParameters(ParameterInfo[] parameters, JsonElement jsonElement, ILogger logger) { var args = new object?[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { var param = parameters[i]; if (jsonElement.TryGetProperty(param.Name!, out var property)) { try { var value = JsonSerializer.Deserialize(property.GetRawText(), param.ParameterType); args[i] = value; } catch (JsonException ex) { logger.LogError(ex, "Failed to deserialize parameter {ParamName}", param.Name); throw new ArgumentException($"Invalid value for parameter '{param.Name}'"); } } else if (param.IsOptional) { args[i] = param.DefaultValue; } else { throw new ArgumentException($"Missing required parameter: {param.Name}"); } } return args; } app.Run();