diff --git a/Engine/Attributes/LCEngineFunctionAttribute.cs b/Engine/Attributes/LCEngineFunctionAttribute.cs new file mode 100644 index 0000000..a9542dd --- /dev/null +++ b/Engine/Attributes/LCEngineFunctionAttribute.cs @@ -0,0 +1,31 @@ +using System; + +namespace LeanCloud.Engine { + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class LCEngineFunctionAttribute : Attribute { + public string FunctionName { + get; + } + + public LCEngineFunctionAttribute(string funcName) { + if (string.IsNullOrEmpty(funcName)) { + throw new ArgumentNullException(nameof(funcName)); + } + FunctionName = funcName; + } + } + + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + public class LCEngineFunctionParameterAttribute : Attribute { + public string ParameterName { + get; + } + + public LCEngineFunctionParameterAttribute(string paramName) { + if (string.IsNullOrEmpty(paramName)) { + throw new ArgumentNullException(nameof(paramName)); + } + ParameterName = paramName; + } + } +} diff --git a/Engine/Attributes/LCEngineObjectHookAttribute.cs b/Engine/Attributes/LCEngineObjectHookAttribute.cs new file mode 100644 index 0000000..44305bc --- /dev/null +++ b/Engine/Attributes/LCEngineObjectHookAttribute.cs @@ -0,0 +1,27 @@ +using System; + +namespace LeanCloud.Engine { + public enum LCEngineObjectHookType { + BeforeSave, + AfterSave, + BeforeUpdate, + AfterUpdate, + BeforeDelete, + AfterDelete + } + + public class LCEngineObjectHookAttribute : Attribute { + public string ClassName { + get; + } + + public LCEngineObjectHookType HookType { + get; + } + + public LCEngineObjectHookAttribute(string className, LCEngineObjectHookType hookType) { + ClassName = className; + HookType = hookType; + } + } +} diff --git a/Engine/Attributes/LCEngineRealtimeHookAttribute.cs b/Engine/Attributes/LCEngineRealtimeHookAttribute.cs new file mode 100644 index 0000000..8c23d06 --- /dev/null +++ b/Engine/Attributes/LCEngineRealtimeHookAttribute.cs @@ -0,0 +1,32 @@ +using System; + +namespace LeanCloud.Engine { + public enum LCEngineRealtimeHookType { + // 消息 + MessageReceived, + MessageSent, + MessageUpdate, + ReceiversOffline, + // 对话 + ConversationStart, + ConversationStarted, + ConversationAdd, + ConversationAdded, + ConversationRemove, + ConversationRemoved, + ConversationUpdate, + // 客户端 + ClientOnline, + ClientOffline, + } + + public class LCEngineRealtimeHookAttribute : Attribute { + public LCEngineRealtimeHookType HookType { + get; + } + + public LCEngineRealtimeHookAttribute(LCEngineRealtimeHookType hookType) { + HookType = hookType; + } + } +} diff --git a/Engine/Attributes/LCEngineUserHookAttribute.cs b/Engine/Attributes/LCEngineUserHookAttribute.cs new file mode 100644 index 0000000..00146d3 --- /dev/null +++ b/Engine/Attributes/LCEngineUserHookAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace LeanCloud.Engine { + public enum LCEngineUserHookType { + SMS, + Email, + OnLogin + } + + public class LCEngineUserHookAttribute : Attribute { + public LCEngineUserHookType HookType { + get; + } + + public LCEngineUserHookAttribute(LCEngineUserHookType hookType) { + HookType = hookType; + } + } +} diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj new file mode 100644 index 0000000..d78acc6 --- /dev/null +++ b/Engine/Engine.csproj @@ -0,0 +1,14 @@ + + + + netcoreapp3.1 + + + + + + + + + + diff --git a/Engine/Handlers/LCClassHookHandler.cs b/Engine/Handlers/LCClassHookHandler.cs new file mode 100644 index 0000000..20e6351 --- /dev/null +++ b/Engine/Handlers/LCClassHookHandler.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using LeanCloud.Storage.Internal.Object; +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + public class LCClassHookHandler { + private static Dictionary ClassHooks => LCEngine.ClassHooks; + + public static void Hello() { + + } + + public static async Task HandleClassHook(string className, string hookName, HttpRequest request, JsonElement body) { + LCLogger.Debug($"Hook: {className}#{hookName}"); + LCLogger.Debug(body.ToString()); + + LCEngine.CheckHookKey(request); + + string classHookName = GetClassHookName(className, hookName); + if (ClassHooks.TryGetValue(classHookName, out MethodInfo mi)) { + Dictionary dict = LCEngine.Decode(body); + + LCObjectData objectData = LCObjectData.Decode(dict["object"] as Dictionary); + objectData.ClassName = className; + LCObject obj = LCObject.Create(className); + obj.Merge(objectData); + + LCUser user = null; + if (dict.TryGetValue("user", out object userObj) && + userObj != null) { + user = new LCUser(); + user.Merge(LCObjectData.Decode(userObj as Dictionary)); + } + + LCClassHookRequest req = new LCClassHookRequest { + Object = obj, + CurrentUser = user + }; + + LCObject result = await LCEngine.Invoke(mi, req) as LCObject; + if (result != null) { + return LCCloud.Encode(result); + } + } + return default; + } + + private static string GetClassHookName(string className, string hookName) { + switch (hookName) { + case "beforeSave": + return $"__before_save_for_{className}"; + case "afterSave": + return $"__after_save_for_{className}"; + case "beforeUpdate": + return $"__before_update_for_{className}"; + case "afterUpdate": + return $"__after_update_for_{className}"; + case "beforeDelete": + return $"__before_delete_for_{className}"; + case "afterDelete": + return $"__after_delete_for_{className}"; + default: + throw new Exception($"Error hook name: {hookName}"); + } + } + } +} diff --git a/Engine/Handlers/LCFunctionHandler.cs b/Engine/Handlers/LCFunctionHandler.cs new file mode 100644 index 0000000..d632041 --- /dev/null +++ b/Engine/Handlers/LCFunctionHandler.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using LeanCloud.Storage.Internal.Codec; +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + public class LCFunctionHandler { + private static Dictionary Functions => LCEngine.Functions; + + /// + /// 云函数 + /// + /// + /// + /// + /// + public static async Task HandleRun(string funcName, HttpRequest request, JsonElement body) { + LCLogger.Debug($"Run: {funcName}"); + LCLogger.Debug(body.ToString()); + + if (Functions.TryGetValue(funcName, out MethodInfo mi)) { + LCUser currentUser = null; + if (request.Headers.TryGetValue("x-lc-session", out StringValues session)) { + currentUser = await LCUser.BecomeWithSessionToken(session); + } + LCCloudFunctionRequest req = new LCCloudFunctionRequest { + Meta = new LCCloudFunctionRequestMeta { + RemoteAddress = LCEngine.GetIP(request) + }, + Params = LCEngine.Decode(body), + SessionToken = session.ToString(), + User = currentUser + }; + object result = await LCEngine.Invoke(mi, req); + if (result != null) { + return new Dictionary { + { "result", result } + }; + } + } + return default; + } + + /// + /// RPC + /// + /// + /// + /// + /// + public static async Task HandleRPC(string funcName, HttpRequest request, JsonElement body) { + LCLogger.Debug($"RPC: {funcName}"); + LCLogger.Debug(body.ToString()); + + if (Functions.TryGetValue(funcName, out MethodInfo mi)) { + LCUser currentUser = null; + if (request.Headers.TryGetValue("x-lc-session", out StringValues session)) { + currentUser = await LCUser.BecomeWithSessionToken(session); + } + LCCloudRPCRequest req = new LCCloudRPCRequest { + Meta = new LCCloudFunctionRequestMeta { + RemoteAddress = LCEngine.GetIP(request) + }, + Params = LCDecoder.Decode(LCEngine.Decode(body)), + SessionToken = session.ToString(), + User = currentUser + }; + object result = await LCEngine.Invoke(mi, req); + if (result != null) { + return new Dictionary { + { "result", LCCloud.Encode(result) } + }; + } + } + return default; + } + } +} diff --git a/Engine/Handlers/LCPingHandler.cs b/Engine/Handlers/LCPingHandler.cs new file mode 100644 index 0000000..dce428d --- /dev/null +++ b/Engine/Handlers/LCPingHandler.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace LeanCloud.Engine { + public class LCPingHandler { + public static object HandlePing() { + LCLogger.Debug("Ping ~~~"); + return new Dictionary { + { "runtime", "dotnet" }, + { "version", LCApplication.SDKVersion } + }; + } + } +} diff --git a/Engine/Handlers/LCUserHookHandler.cs b/Engine/Handlers/LCUserHookHandler.cs new file mode 100644 index 0000000..eb65197 --- /dev/null +++ b/Engine/Handlers/LCUserHookHandler.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using LeanCloud.Storage.Internal.Object; +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + public class LCUserHookHandler { + private static Dictionary UserHooks => LCEngine.UserHooks; + + public static async Task HandleVerifiedSMS(HttpRequest request, JsonElement body) { + LCLogger.Debug(LCEngine.OnSMSVerified); + LCLogger.Debug(body.ToString()); + + LCEngine.CheckHookKey(request); + + if (UserHooks.TryGetValue(LCEngine.OnSMSVerified, out MethodInfo mi)) { + Dictionary dict = LCEngine.Decode(body); + return await Invoke(mi, dict); + } + return default; + } + + public static async Task HandleVerifiedEmail(HttpRequest request, JsonElement body) { + LCLogger.Debug(LCEngine.OnEmailVerified); + LCLogger.Debug(body.ToString()); + + LCEngine.CheckHookKey(request); + + if (UserHooks.TryGetValue(LCEngine.OnEmailVerified, out MethodInfo mi)) { + Dictionary dict = LCEngine.Decode(body); + return await Invoke(mi, dict); + } + return default; + } + + public static async Task HandleLogin(HttpRequest request, JsonElement body) { + LCLogger.Debug(LCEngine.OnLogin); + LCLogger.Debug(body.ToString()); + + LCEngine.CheckHookKey(request); + + if (UserHooks.TryGetValue(LCEngine.OnLogin, out MethodInfo mi)) { + Dictionary dict = LCEngine.Decode(body); + return await Invoke(mi, dict); + } + return default; + } + + private static async Task Invoke(MethodInfo mi, Dictionary dict) { + LCObjectData objectData = LCObjectData.Decode(dict["object"] as Dictionary); + objectData.ClassName = "_User"; + + LCObject obj = LCObject.Create("_User"); + obj.Merge(objectData); + + LCUserHookRequest req = new LCUserHookRequest { + CurrentUser = obj as LCUser + }; + + return await LCEngine.Invoke(mi, req) as LCObject; + } + } +} diff --git a/Engine/LCEngine.cs b/Engine/LCEngine.cs new file mode 100644 index 0000000..cadbae2 --- /dev/null +++ b/Engine/LCEngine.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Microsoft.Extensions.Primitives; +using LeanCloud.Common; + +namespace LeanCloud.Engine { + public class LCEngine { + const string LCMasterKeyName = "x-avoscloud-master-key"; + const string LCHookKeyName = "x-lc-hook-key"; + + const string BeforeSave = "__before_save_for_"; + const string AfterSave = "__after_save_for_"; + const string BeforeUpdate = "__before_update_for_"; + const string AfterUpdate = "__after_update_for_"; + const string BeforeDelete = "__before_delete_for_"; + const string AfterDelete = "__after_delete_for_"; + + internal const string OnSMSVerified = "__on_verified_sms"; + internal const string OnEmailVerified = "__on_verified_email"; + internal const string OnLogin = "__on_login__User"; + + const string ClientOnline = "_clientOnline"; + const string ClientOffline = "_clientOffline"; + + const string MessageSent = "_messageSent"; + const string MessageReceived = "_messageReceived"; + const string ReceiversOffline = "_receiversOffline"; + const string MessageUpdate = "_messageUpdate"; + + const string ConversationStart = "_conversationStart"; + const string ConversationStarted = "_conversationStarted"; + const string ConversationAdd = "_conversationAdd"; + const string ConversationAdded = "_conversationAdded"; + const string ConversationRemove = "_conversationRemove"; + const string ConversationRemoved = "_conversationRemoved"; + const string ConversationUpdate = "_conversationUpdate"; + + public static Dictionary Functions = new Dictionary(); + public static Dictionary ClassHooks = new Dictionary(); + public static Dictionary UserHooks = new Dictionary(); + + public static void Initialize() { + // 获取环境变量 + LCLogger.Debug("-------------------------------------------------"); + PrintEnvironmentVar("LEANCLOUD_APP_ID"); + PrintEnvironmentVar("LEANCLOUD_APP_KEY"); + PrintEnvironmentVar("LEANCLOUD_APP_MASTER_KEY"); + PrintEnvironmentVar("LEANCLOUD_APP_HOOK_KEY"); + PrintEnvironmentVar("LEANCLOUD_API_SERVER"); + PrintEnvironmentVar("LEANCLOUD_APP_PROD"); + PrintEnvironmentVar("LEANCLOUD_APP_ENV"); + PrintEnvironmentVar("LEANCLOUD_APP_INSTANCE"); + PrintEnvironmentVar("LEANCLOUD_REGION"); + PrintEnvironmentVar("LEANCLOUD_APP_ID"); + PrintEnvironmentVar("LEANCLOUD_APP_DOMAIN"); + PrintEnvironmentVar("LEANCLOUD_APP_PORT"); + LCLogger.Debug("-------------------------------------------------"); + + LCApplication.Initialize(Environment.GetEnvironmentVariable("LEANCLOUD_APP_ID"), + Environment.GetEnvironmentVariable("LEANCLOUD_APP_KEY"), + Environment.GetEnvironmentVariable("LEANCLOUD_API_SERVER")); + + Assembly assembly = Assembly.GetCallingAssembly(); + ClassHooks = assembly.GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(m => m.GetCustomAttribute() != null) + .ToDictionary(mi => { + LCEngineObjectHookAttribute attr = mi.GetCustomAttribute(); + switch (attr.HookType) { + case LCEngineObjectHookType.BeforeSave: + return $"{BeforeSave}{attr.ClassName}"; + case LCEngineObjectHookType.AfterSave: + return $"{AfterSave}{attr.ClassName}"; + case LCEngineObjectHookType.BeforeUpdate: + return $"{BeforeUpdate}{attr.ClassName}"; + case LCEngineObjectHookType.AfterUpdate: + return $"{AfterUpdate}{attr.ClassName}"; + case LCEngineObjectHookType.BeforeDelete: + return $"{BeforeDelete}{attr.ClassName}"; + case LCEngineObjectHookType.AfterDelete: + return $"{AfterDelete}{attr.ClassName}"; + default: + throw new Exception($"Error hook type: {attr.HookType}"); + } + }); + + UserHooks = assembly.GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(m => m.GetCustomAttribute() != null) + .ToDictionary(mi => { + LCEngineUserHookAttribute attr = mi.GetCustomAttribute(); + switch (attr.HookType) { + case LCEngineUserHookType.SMS: + return OnSMSVerified; + case LCEngineUserHookType.Email: + return OnEmailVerified; + case LCEngineUserHookType.OnLogin: + return OnLogin; + default: + throw new Exception($"Error hook type: {attr.HookType}"); + } + }); + + Functions = assembly.GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(m => m.GetCustomAttribute() != null) + .ToDictionary(mi => mi.GetCustomAttribute().FunctionName); + + assembly.GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(m => m.GetCustomAttribute() != null) + .ToDictionary(mi => { + LCEngineRealtimeHookAttribute attr = mi.GetCustomAttribute(); + switch (attr.HookType) { + case LCEngineRealtimeHookType.ClientOnline: + return ClientOnline; + case LCEngineRealtimeHookType.ClientOffline: + return ClientOffline; + case LCEngineRealtimeHookType.MessageSent: + return MessageSent; + case LCEngineRealtimeHookType.MessageReceived: + return MessageReceived; + case LCEngineRealtimeHookType.ReceiversOffline: + return ReceiversOffline; + case LCEngineRealtimeHookType.MessageUpdate: + return MessageUpdate; + case LCEngineRealtimeHookType.ConversationStart: + return ConversationStart; + case LCEngineRealtimeHookType.ConversationStarted: + return ConversationStarted; + case LCEngineRealtimeHookType.ConversationAdd: + return ConversationAdd; + case LCEngineRealtimeHookType.ConversationAdded: + return ConversationAdded; + case LCEngineRealtimeHookType.ConversationRemove: + return ConversationRemove; + case LCEngineRealtimeHookType.ConversationRemoved: + return ConversationRemoved; + case LCEngineRealtimeHookType.ConversationUpdate: + return ConversationUpdate; + default: + throw new Exception($"Error hook type: {attr.HookType}"); + } + }) + .ToList() + .ForEach(item => { + Functions.TryAdd(item.Key, item.Value); + }); + } + + public static void PrintEnvironmentVar(string key) { + LCLogger.Debug($"{key} : {Environment.GetEnvironmentVariable(key)}"); + } + + internal static async Task Invoke(MethodInfo mi, object request) { + try { + object[] ps = new object[] { request }; + if (mi.ReturnType == typeof(Task) || + (mi.ReturnType.IsGenericType && mi.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))) { + Task task = mi.Invoke(null, ps) as Task; + await task; + return task.GetType().GetProperty("Result")?.GetValue(task); + } + return mi.Invoke(null, ps); + } catch (TargetInvocationException e) { + throw e.InnerException; + } + } + + internal static Dictionary Decode(JsonElement jsonElement) { + string json = System.Text.Json.JsonSerializer.Serialize(jsonElement); + Dictionary dict = JsonConvert.DeserializeObject>(json, + LCJsonConverter.Default); + return dict; + } + + internal static string GetIP(HttpRequest request) { + if (request.Headers.TryGetValue("x-real-ip", out StringValues ip)) { + return ip.ToString(); + } + if (request.Headers.TryGetValue("x-forwarded-for", out StringValues forward)) { + return forward.ToString(); + } + return request.HttpContext.Connection.RemoteIpAddress.ToString(); + } + + internal static void CheckMasterKey(HttpRequest request) { + if (!request.Headers.TryGetValue(LCMasterKeyName, out StringValues masterKey)) { + throw new Exception("No master key"); + } + if (!masterKey.Equals(Environment.GetEnvironmentVariable("LEANCLOUD_APP_MASTER_KEY"))) { + throw new Exception("Mismatch master key"); + } + } + + internal static void CheckHookKey(HttpRequest request) { + if (!request.Headers.TryGetValue(LCHookKeyName, out StringValues hookKey)) { + throw new Exception("No hook key"); + } + if (!hookKey.Equals(Environment.GetEnvironmentVariable("LEANCLOUD_APP_HOOK_KEY"))) { + throw new Exception("Mismatch hook key"); + } + } + + public static object GetFunctions(HttpRequest request) { + CheckMasterKey(request); + + List functions = new List(); + functions.AddRange(Functions.Keys); + functions.AddRange(ClassHooks.Keys); + functions.AddRange(UserHooks.Keys); + foreach (string func in functions) { + LCLogger.Debug(func); + } + + return new Dictionary> { + { "result", functions } + }; + } + } +} diff --git a/Engine/Requests/LCClassHookRequest.cs b/Engine/Requests/LCClassHookRequest.cs new file mode 100644 index 0000000..553cff0 --- /dev/null +++ b/Engine/Requests/LCClassHookRequest.cs @@ -0,0 +1,13 @@ +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + public class LCClassHookRequest { + public LCObject Object { + get; set; + } + + public LCUser CurrentUser { + get; set; + } + } +} diff --git a/Engine/Requests/LCCloudFunctionRequest.cs b/Engine/Requests/LCCloudFunctionRequest.cs new file mode 100644 index 0000000..d4ecb3b --- /dev/null +++ b/Engine/Requests/LCCloudFunctionRequest.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + public class LCCloudFunctionRequestMeta { + public string RemoteAddress { + get; set; + } + } + + public class LCCloudFunctionRequest { + public LCCloudFunctionRequestMeta Meta { + get; set; + } + + public Dictionary Params { + get; set; + } + + public LCUser User { + get; set; + } + + public string SessionToken { + get; set; + } + } +} diff --git a/Engine/Requests/LCCloudRPCRequest.cs b/Engine/Requests/LCCloudRPCRequest.cs new file mode 100644 index 0000000..6c0b65d --- /dev/null +++ b/Engine/Requests/LCCloudRPCRequest.cs @@ -0,0 +1,21 @@ +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + public class LCCloudRPCRequest { + public LCCloudFunctionRequestMeta Meta { + get; set; + } + + public object Params { + get; set; + } + + public LCUser User { + get; set; + } + + public string SessionToken { + get; set; + } + } +} diff --git a/Engine/Requests/LCUserHookRequest.cs b/Engine/Requests/LCUserHookRequest.cs new file mode 100644 index 0000000..8e4ba97 --- /dev/null +++ b/Engine/Requests/LCUserHookRequest.cs @@ -0,0 +1,9 @@ +using LeanCloud.Storage; + +namespace LeanCloud.Engine { + public class LCUserHookRequest { + public LCUser CurrentUser { + get; set; + } + } +}