Created
August 26, 2021 22:07
-
-
Save luttje/8266f5e972fa70dafc8cd2d79286cdbe to your computer and use it in GitHub Desktop.
Jint Allowlist to help decide which .NET members can be accessed from JavaScript (this is untested code, I copied this in part from a larger codebase)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[System.AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method, Inherited = true)] | |
public class ScriptAllowedAttribute : Attribute | |
{ | |
public ScriptAllowedAttribute() { } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[System.AttributeUsage(AttributeTargets.Class)] | |
public class ScriptAllowlistEnabledAttribute : Attribute | |
{ | |
public ScriptAllowlistEnabledAttribute() { } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ScriptManager | |
{ | |
private Engine engine; | |
public ScriptManager() | |
{ | |
PrepareEngine(); | |
engine.Evaluate("UI.createWindow() != null"); // Should be true | |
engine.Evaluate("UI.someOtherMethod()"); // Should fail | |
} | |
private void PrepareEngine() | |
{ | |
engine = new Engine((_, opts) => | |
{ | |
opts.SetMemberAccessor(GetObjectMember); | |
}); | |
var uiSystem = new UI(); | |
engine.SetValue("UI", uiSystem); | |
} | |
private static JsValue GetObjectMember(Engine engine, object target, string memberName) | |
{ | |
var type = target.GetType(); | |
// Only classes marked with [ScriptAllowlistEnabled] attribute shall be filtered | |
if (type.GetCustomAttribute<ScriptAllowlistEnabledAttribute>() == null) | |
return null; | |
// Convert lowerCamelCase to UpperCamelCase | |
var builder = new StringBuilder(); | |
builder.Append(char.ToUpper(memberName[0])); | |
if (memberName.Length > 1) | |
builder.Append(memberName.Substring(1)); | |
var upperCamelCaseMemberName = builder.ToString(); | |
var members = type.GetMember(upperCamelCaseMemberName); | |
MemberInfo member = null; | |
// Find member with [ScriptAllowed] attribute | |
for (var i = 0; i < members.Length; i++) | |
{ | |
if (members[i].GetCustomAttribute<ScriptAllowedAttribute>() != null) | |
if (member == null) | |
member = members[i]; | |
else | |
// TODO: Implement checking overloaded method somehow | |
throw new NotImplementedException($"Members name ambigous. Multiple members with name `{upperCamelCaseMemberName}` are marked [ScriptAccessible]."); | |
} | |
// If no members exist marked [ScriptAllowed], do not allow accessing them | |
if (member == null) | |
return JsValue.Undefined; | |
// Return the value from the member | |
switch (member.MemberType) | |
{ | |
case MemberTypes.Field: | |
return JsValue.FromObject(engine, ((FieldInfo)member).GetValue(target)); | |
case MemberTypes.Property: | |
return JsValue.FromObject(engine, ((PropertyInfo)member).GetValue(target)); | |
case MemberTypes.Method: | |
case MemberTypes.Constructor: | |
var method = (MethodBase)member; | |
var methodParameters = method.GetParameters(); | |
// TODO: This seems hacky? | |
return new ClrFunctionInstance(engine, memberName, (thisObject, arguments) => | |
{ | |
return CallMethod(engine, method, methodParameters, (ObjectWrapper)thisObject, arguments); | |
}, methodParameters.Length, PropertyFlag.Configurable); // Why PropertyFlag.Configurable?? No idea. | |
default: | |
throw new NotImplementedException("Can't access this type of member from JavaScript yet!"); | |
} | |
} | |
// Based on Jint\Runtime\Interop\MethodDescriptor.cs:105 in Jint | |
public static JsValue CallMethod(Engine engine, MethodBase member, ParameterInfo[] methodParameters, ObjectWrapper instance, JsValue[] arguments) | |
{ | |
var parameters = new object[arguments.Length]; | |
for (var i = 0; i < arguments.Length; i++) | |
{ | |
var parameterType = methodParameters[i].ParameterType; | |
if (typeof(JsValue).IsAssignableFrom(parameterType)) | |
{ | |
parameters[i] = arguments[i]; | |
} | |
else | |
{ | |
parameters[i] = engine.ClrTypeConverter.Convert( | |
arguments[i].ToObject(), | |
parameterType, | |
System.Globalization.CultureInfo.InvariantCulture); | |
} | |
} | |
if (member is MethodInfo m) | |
{ | |
var retVal = m.Invoke(instance.Target, parameters); | |
return JsValue.FromObject(engine, retVal); | |
} | |
else if (member is ConstructorInfo c) | |
{ | |
var retVal = c.Invoke(parameters); | |
return JsValue.FromObject(engine, retVal); | |
} | |
else | |
{ | |
throw new Exception("Method is unknown type"); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ScriptAllowlistEnabled] | |
public class UI | |
{ | |
public UI() | |
{ | |
} | |
// This method can be accessed from JavaScript (through 'createWindow' on a UI object) | |
[ScriptAllowed] | |
public Window CreateWindow() | |
{ | |
return OtherUserInterfaceClass.CreateWindow(); | |
} | |
// This method can't be accessed | |
public int SomeOtherMethod() => 9001; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment