Files
S8n.MySolutionTemplate/MyCompany.MyProject.BackendApi/Program.cs
2026-02-11 16:12:04 +03:00

410 lines
14 KiB
C#

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<ComponentDefinition>();
var presets = new List<PresetDefinition>();
// 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<string>();
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<PresetDefinition>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (preset != null)
{
presets.Add(preset);
}
}
else
{
var component = JsonSerializer.Deserialize<ComponentDefinition>(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<RuntimeRequest> 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<object?> 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<T>
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();