WiP
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Scalar.AspNetCore;
|
||||
using MyCompany.MyProject.BackendApi.Models;
|
||||
using System.Security;
|
||||
|
||||
// Ensure S8n.Components.Packages assembly is loaded
|
||||
_ = typeof(S8n.Components.Basics.Calculator).Assembly;
|
||||
@@ -25,185 +24,197 @@ if (app.Environment.IsDevelopment())
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
var summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
app.MapGet("/weatherforecast", () =>
|
||||
{
|
||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
||||
new WeatherForecast
|
||||
(
|
||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
Random.Shared.Next(-20, 55),
|
||||
summaries[Random.Shared.Next(summaries.Length)]
|
||||
))
|
||||
.ToArray();
|
||||
return forecast;
|
||||
})
|
||||
.WithName("GetWeatherForecast");
|
||||
|
||||
// Component registry endpoint
|
||||
app.MapGet("/api/components", () =>
|
||||
{
|
||||
var components = new List<ComponentDefinition>();
|
||||
var componentsPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "components");
|
||||
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");
|
||||
}
|
||||
|
||||
if (Directory.Exists(componentsPath))
|
||||
// Load presets from components_user/ directory
|
||||
var userComponentsPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "components_user");
|
||||
if (!Directory.Exists(userComponentsPath))
|
||||
{
|
||||
var jsonFiles = Directory.GetFiles(componentsPath, "*.json", SearchOption.AllDirectories);
|
||||
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);
|
||||
var component = JsonSerializer.Deserialize<ComponentDefinition>(json, new JsonSerializerOptions
|
||||
|
||||
// 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)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
if (component != null)
|
||||
var preset = JsonSerializer.Deserialize<PresetDefinition>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
if (preset != null)
|
||||
{
|
||||
presets.Add(preset);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
components.Add(component);
|
||||
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 file {file}: {ex.Message}");
|
||||
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) =>
|
||||
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
|
||||
{
|
||||
// Get the type from the class name
|
||||
var type = Type.GetType(request.ClassName);
|
||||
if (type == null)
|
||||
// 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)
|
||||
{
|
||||
// Try to find in loaded assemblies
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
logger.LogWarning("Invalid input parameters received");
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
type = assembly.GetType(request.ClassName);
|
||||
if (type != null) break;
|
||||
}
|
||||
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)
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
logger.LogWarning("Class not found: {ClassName}", request.ClassName);
|
||||
return Results.NotFound(new RuntimeResponse
|
||||
{
|
||||
Error = $"Class not found: {request.ClassName}"
|
||||
});
|
||||
}
|
||||
|
||||
// Create instance
|
||||
var instance = Activator.CreateInstance(type);
|
||||
// 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
|
||||
var method = type.GetMethod(request.MethodName);
|
||||
// Get the method with security checks
|
||||
var method = type.GetMethod(request.MethodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
|
||||
if (method == null)
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
logger.LogWarning("Method not found: {MethodName} in type {Type}", request.MethodName, type.FullName);
|
||||
return Results.NotFound(new RuntimeResponse
|
||||
{
|
||||
Error = $"Method not found: {request.MethodName}"
|
||||
});
|
||||
}
|
||||
|
||||
// Parse inputs and invoke method
|
||||
var inputs = request.Inputs as JsonElement?;
|
||||
object? result;
|
||||
|
||||
if (inputs.HasValue)
|
||||
// Security: Verify method is safe to execute
|
||||
if (!IsSafeExecutableMethod(method))
|
||||
{
|
||||
// Get method parameters
|
||||
var parameters = method.GetParameters();
|
||||
var args = new List<object?>();
|
||||
|
||||
foreach (var param in parameters)
|
||||
logger.LogWarning("Unsafe method attempted: {Method} in type {Type}", method.Name, type.FullName);
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
if (inputs.Value.TryGetProperty(param.Name!, out var property))
|
||||
{
|
||||
var value = JsonSerializer.Deserialize(property.GetRawText(), param.ParameterType);
|
||||
args.Add(value);
|
||||
}
|
||||
else if (param.IsOptional)
|
||||
{
|
||||
args.Add(param.DefaultValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
Error = $"Missing required parameter: {param.Name}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result = method.Invoke(instance, args.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
result = method.Invoke(instance, null);
|
||||
Error = "Method execution is not allowed"
|
||||
});
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"Invocation result type: {result?.GetType()}");
|
||||
// If method returns a Task, await it
|
||||
if (result is Task taskResult)
|
||||
{
|
||||
Console.Error.WriteLine($"Detected Task, awaiting...");
|
||||
await taskResult.ConfigureAwait(false);
|
||||
Console.Error.WriteLine($"Task completed, status: {taskResult.Status}");
|
||||
// Execute method with proper parameter binding
|
||||
var result = await ExecuteMethodSafely(instance, method, request.Inputs, logger);
|
||||
|
||||
// If it's a Task<T>, get the result via reflection
|
||||
var resultType = taskResult.GetType();
|
||||
Console.Error.WriteLine($"Task result type: {resultType}");
|
||||
if (typeof(Task).IsAssignableFrom(resultType))
|
||||
{
|
||||
Console.Error.WriteLine($"Generic Task detected");
|
||||
// Get the Result property
|
||||
var resultProperty = resultType.GetProperty("Result");
|
||||
if (resultProperty != null)
|
||||
{
|
||||
result = resultProperty.GetValue(taskResult);
|
||||
Console.Error.WriteLine($"Result value: {result}");
|
||||
Console.Error.WriteLine($"Result value type: {result?.GetType()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"Result property not found");
|
||||
result = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Task (non-generic) returns nothing
|
||||
Console.Error.WriteLine($"Non-generic Task, setting result to null");
|
||||
result = null;
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"Final result: {result}");
|
||||
return Results.Ok(new RuntimeResponse
|
||||
{
|
||||
Outputs = result
|
||||
@@ -211,69 +222,188 @@ app.MapPost("/api/runtime/execute", async (RuntimeRequest request) =>
|
||||
}
|
||||
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)
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
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();
|
||||
|
||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||
{
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
|
||||
record RuntimeRequest
|
||||
{
|
||||
public string ClassName { get; set; } = string.Empty;
|
||||
public string MethodName { get; set; } = string.Empty;
|
||||
public object? Inputs { get; set; }
|
||||
}
|
||||
|
||||
record RuntimeResponse
|
||||
{
|
||||
public object? Outputs { get; set; }
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
record ComponentDefinition
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public List<ComponentInput> Inputs { get; set; } = new();
|
||||
public List<ComponentOutput> Outputs { get; set; } = new();
|
||||
public string Source { get; set; } = string.Empty;
|
||||
public string Class { get; set; } = string.Empty;
|
||||
public List<string> Methods { get; set; } = new();
|
||||
public string Gui { get; set; } = string.Empty;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public List<string> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
record ComponentInput
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<string>? Enum { get; set; }
|
||||
public bool Required { get; set; } = true;
|
||||
}
|
||||
|
||||
record ComponentOutput
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user