using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Messages.InboundApi;
using ScadaLink.Commons.Types;
namespace ScadaLink.InboundAPI;
///
/// WP-3: Executes the C# script associated with an inbound API method.
/// Compiles method scripts via Roslyn and caches compiled delegates.
///
public class InboundScriptExecutor
{
private readonly ILogger _logger;
// InboundAPI-001: this executor is registered as a singleton and its handler cache
// is read and written from concurrent ASP.NET request threads. A plain Dictionary is
// not safe for concurrent read/write, so a ConcurrentDictionary is used throughout.
private readonly ConcurrentDictionary>> _scriptHandlers = new();
// InboundAPI-009: a script that fails to compile (or violates the trust model)
// is recorded here so it is compiled at most once. Without this, every subsequent
// request for a broken method re-runs the expensive Roslyn compilation — a CPU
// amplification vector since the inbound API has no rate limiting. The entry is
// cleared whenever the method is (re)compiled via CompileAndRegister.
private readonly ConcurrentDictionary _knownBadMethods = new();
private readonly IServiceProvider _serviceProvider;
public InboundScriptExecutor(ILogger logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
///
/// Registers a compiled script handler for a method name.
///
public void RegisterHandler(string methodName, Func> handler)
{
_scriptHandlers[methodName] = handler;
}
///
/// Removes a compiled script handler for a method name.
///
public void RemoveHandler(string methodName)
{
_scriptHandlers.TryRemove(methodName, out _);
}
///
/// Compiles and registers a single API method script. Returns false if the
/// script is empty, fails Roslyn compilation, or violates the script trust model.
///
public bool CompileAndRegister(ApiMethod method)
{
var handler = Compile(method);
if (handler == null)
{
// InboundAPI-009: record the failure so the lazy-compile path does not
// keep recompiling a broken script on every request.
_knownBadMethods[method.Name] = 0;
return false;
}
// The method definition was (re)compiled successfully — drop any stale
// failure record so a fixed script is no longer treated as bad.
_knownBadMethods.TryRemove(method.Name, out _);
return Register(method.Name, handler);
}
private bool Register(string methodName, Func> handler)
{
_scriptHandlers[methodName] = handler;
return true;
}
///
/// Compiles a single API method script into an executable handler. Returns
/// null when the script is missing, fails to compile, or violates the
/// script trust model (InboundAPI-005). Does not mutate the handler cache.
///
private Func>? Compile(ApiMethod method)
{
if (string.IsNullOrWhiteSpace(method.Script))
{
_logger.LogWarning("API method {Method} has no script code", method.Name);
return null;
}
// InboundAPI-005: enforce the script trust model before compiling. Roslyn
// scripting performs no API allow/deny-listing, so forbidden namespaces must
// be rejected statically or the script could reach the host process.
var violations = ForbiddenApiChecker.FindViolations(method.Script);
if (violations.Count > 0)
{
_logger.LogWarning(
"API method {Method} script rejected — trust model violation(s): {Violations}",
method.Name, string.Join("; ", violations));
return null;
}
try
{
var scriptOptions = ScriptOptions.Default
.WithReferences(
typeof(object).Assembly,
typeof(Enumerable).Assembly,
typeof(Dictionary<,>).Assembly,
typeof(RouteHelper).Assembly,
typeof(ScriptParameters).Assembly,
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly)
.WithImports(
"System",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks");
var compiled = CSharpScript.Create