diff --git a/src/ActionContext.cs b/src/ActionContext.cs index e91b329..77c0848 100644 --- a/src/ActionContext.cs +++ b/src/ActionContext.cs @@ -1,11 +1,12 @@ -using System; +using BeetleX.Tracks; +using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace BeetleX.FastHttpApi { - public class ActionContext + public class ActionContext : IDisposable { internal ActionContext(ActionHandler handler, IHttpContext context, ActionHandlerFactory actionHandlerFactory) @@ -14,33 +15,49 @@ internal ActionContext(ActionHandler handler, IHttpContext context, ActionHandle mFilters = handler.Filters; HttpContext = context; ActionHandlerFactory = actionHandlerFactory; - Parameters = handler.GetParameters(context); Controller = handler.Controller; - if (handler.InstanceType != InstanceType.Single) + if (HttpContext.WebSocket) { - if (handler.InstanceType == InstanceType.Session) + ActionType = "WS"; + } + else + { + ActionType = "HTTP"; + } + + } + + public void Init() + { + FilterInit(); + Parameters = Handler.GetParameters(HttpContext, this); + if (Handler.InstanceType != InstanceType.Single) + { + if (Handler.InstanceType == InstanceType.Session) { - var factory = SessionControllerFactory.GetFactory(context.Session); - Controller = factory[handler.ControllerUID]; + var factory = SessionControllerFactory.GetFactory(HttpContext.Session); + Controller = factory[Handler.ControllerUID]; if (Controller == null) { - Controller = actionHandlerFactory.GetController(handler.ControllerType, context); + Controller = ActionHandlerFactory.GetController(Handler.ControllerType, HttpContext); if (Controller == null) - Controller = Activator.CreateInstance(handler.ControllerType); - factory[handler.ControllerUID] = Controller; + Controller = Activator.CreateInstance(Handler.ControllerType); + factory[Handler.ControllerUID] = Controller; } } else { - Controller = actionHandlerFactory.GetController(handler.ControllerType, context); + Controller = ActionHandlerFactory.GetController(Handler.ControllerType, HttpContext); if (Controller == null) - Controller = Activator.CreateInstance(handler.ControllerType); + Controller = Activator.CreateInstance(Handler.ControllerType); } } if (Controller == null) - Controller = handler.Controller; + Controller = Handler.Controller; } + private string ActionType = ""; + private List mFilters; public object[] Parameters { get; private set; } @@ -55,6 +72,8 @@ internal ActionContext(ActionHandler handler, IHttpContext context, ActionHandle public Exception Exception { get; set; } + public bool HasError { get; private set; } + public object Controller { get; set; } public string GetCacheKey() @@ -70,15 +89,21 @@ private void OnExecute(IActionResultHandler resultHandler) { try { - Result = Handler.Invoke(Controller, HttpContext, ActionHandlerFactory, Parameters); + using (CodeTrackFactory.Track(Handler.SourceUrl, CodeTrackLevel.Function, null, "Execute")) + { + Result = Handler.Invoke(Controller, HttpContext, ActionHandlerFactory, Parameters); + } + } catch (Exception error) { Exception = error; + HasError = true; } finally { FilterExecuted(); + ParametersDisposed(); } } if (Exception != null) @@ -87,7 +112,12 @@ private void OnExecute(IActionResultHandler resultHandler) resultHandler.Error(Exception); } else - resultHandler.Success(Result); + { + using (CodeTrackFactory.Track(Handler.SourceUrl, CodeTrackLevel.Function, null, "Responsed")) + { + resultHandler.Success(Result); + } + } } catch (Exception e_) { @@ -96,7 +126,11 @@ private void OnExecute(IActionResultHandler resultHandler) } finally { - DisposedController(); + using (CodeTrackFactory.Track(Handler.SourceUrl, CodeTrackLevel.Function, null, "Disposed")) + { + DisposedController(); + Dispose(); + } } } @@ -108,20 +142,26 @@ private async Task OnAsyncExecute(IActionResultHandler resultHandler) { try { - var task = (Task)Handler.Invoke(Controller, HttpContext, ActionHandlerFactory, Parameters); - await task; - if (Handler.PropertyHandler != null) - Result = Handler.PropertyHandler.Get(task); - else - Result = null; + + using (CodeTrackFactory.Track(Handler.SourceUrl, CodeTrackLevel.Function, null, "Execute")) + { + var task = (Task)Handler.Invoke(Controller, HttpContext, ActionHandlerFactory, Parameters); + await task; + if (Handler.PropertyHandler != null) + Result = Handler.PropertyHandler.Get(task); + else + Result = null; + } } catch (Exception error) { Exception = error; + HasError = true; } finally { FilterExecuted(); + ParametersDisposed(); } } if (Exception != null) @@ -130,7 +170,12 @@ private async Task OnAsyncExecute(IActionResultHandler resultHandler) resultHandler.Error(Exception); } else - resultHandler.Success(Result); + { + using (CodeTrackFactory.Track(Handler.SourceUrl, CodeTrackLevel.Function, null, "Response")) + { + resultHandler.Success(Result); + } + } } catch (Exception e_) { @@ -139,7 +184,11 @@ private async Task OnAsyncExecute(IActionResultHandler resultHandler) } finally { - DisposedController(); + using (CodeTrackFactory.Track(Handler.SourceUrl, CodeTrackLevel.Function, null, "Disposed")) + { + DisposedController(); + Dispose(); + } } } @@ -157,7 +206,7 @@ private void DisposedController() if (HttpContext.Server.EnableLog(EventArgs.LogType.Error)) { var request = HttpContext.Request; - HttpContext.Server.Log(EventArgs.LogType.Error, + HttpContext.Server.Log(EventArgs.LogType.Error, request.Session, $"HTTP {request.RemoteIPAddress} {request.Method} {request.BaseUrl} controller disposed error {e_.Message}@{e_.StackTrace}"); } } @@ -166,13 +215,15 @@ private void DisposedController() struct ActionTask : IEventWork { - public ActionTask(ActionContext context, IActionResultHandler resultHandler) + public ActionTask(ActionContext context, IActionResultHandler resultHandler, TaskCompletionSource completionSource) { Context = context; ResultHandler = resultHandler; - + CompletionSource = completionSource; } + public TaskCompletionSource CompletionSource { get; set; } + public ActionContext Context { get; set; } public IActionResultHandler ResultHandler { get; set; } @@ -184,19 +235,27 @@ public void Dispose() public async Task Execute() { - if (Context.Handler.Async) + try { - await Context.OnAsyncExecute(ResultHandler); + if (Context.Handler.Async) + { + await Context.OnAsyncExecute(ResultHandler); + } + else + { + Context.OnExecute(ResultHandler); + } } - else + finally { - Context.OnExecute(ResultHandler); + + CompletionSource?.TrySetResult(new object()); } } } - internal void Execute(IActionResultHandler resultHandler) + internal async Task Execute(IActionResultHandler resultHandler) { if (Handler.ValidateRPS()) { @@ -205,7 +264,7 @@ internal void Execute(IActionResultHandler resultHandler) { if (Handler.Async) { - OnAsyncExecute(resultHandler); + await OnAsyncExecute(resultHandler); } else { @@ -214,12 +273,13 @@ internal void Execute(IActionResultHandler resultHandler) } else { - ActionTask actionTask = new ActionTask(this, resultHandler); + ActionTask actionTask = new ActionTask(this, resultHandler, new TaskCompletionSource()); var queue = Handler.ThreadQueue.GetQueue(this.HttpContext); if (Handler.ThreadQueue.Enabled(queue)) { this.HttpContext.Queue = queue; queue.Enqueue(actionTask); + await actionTask.CompletionSource.Task; } else { @@ -238,16 +298,81 @@ internal void Execute(IActionResultHandler resultHandler) private int mFilterIndex; + private void FilterInit() + { + if (mFilters.Count > 0) + { + for (int i = 0; i < mFilters.Count; i++) + { + mFilters[i].Init(HttpContext, Handler); + } + } + } + + private void FilterDisposed() + { + if (mFilters.Count > 0) + { + for (int i = 0; i < mFilters.Count; i++) + { + try + { + mFilters[i].Disposed(this); + } + catch (Exception e_) + { + if (HttpContext.Server.EnableLog(EventArgs.LogType.Error)) + { + var request = HttpContext.Request; + HttpContext.Server.Log(EventArgs.LogType.Error, request.Session, + $"HTTP {request.RemoteIPAddress} {request.Method} {request.BaseUrl} {mFilters[i]} filter disposed error {e_.Message}@{e_.StackTrace}"); + } + } + } + } + } + + private void ParametersDisposed() + { + if (Parameters != null) + { + for (int i = 0; i < Parameters.Length; i++) + { + try + { + if (Parameters[i] != null && Parameters[i] is IActionParameter parameter) + { + parameter.Dispose(); + } + } + catch (Exception e_) + { + this.Exception = e_; + if (HttpContext.Server.EnableLog(EventArgs.LogType.Error)) + { + var request = HttpContext.Request; + HttpContext.Server.Log(EventArgs.LogType.Error, request.Session, + $"HTTP {request.RemoteIPAddress} {request.Method} {request.BaseUrl} {Parameters[i]} parameter disposed error {e_.Message}@{e_.StackTrace} inner error:{e_.InnerException?.Message}"); + } + } + } + } + } + private bool FilterExecuting() { if (mFilters.Count > 0) { for (int i = 0; i < mFilters.Count; i++) { - bool result = mFilters[i].Executing(this); - mFilterIndex++; - if (!result) - return false; + using (CodeTrackFactory.Track(Handler.SourceUrl, CodeTrackLevel.Function, null, + "Filter", mFilters[i].GetType().Name, "Executing")) + { + bool result = mFilters[i].Executing(this); + mFilterIndex++; + if (!result) + return false; + } } } return true; @@ -260,9 +385,26 @@ private void FilterExecuted() int start = mFilterIndex - 1; for (int i = start; i >= 0; i--) { - mFilters[i].Executed(this); + using (CodeTrackFactory.Track(Handler.SourceUrl, CodeTrackLevel.Function, null, + "Filter", mFilters[i].GetType().Name, "Executed")) + { + mFilters[i].Executed(this); + } } } } + + private bool mIsDisposed = false; + + public void Dispose() + { + if (!mIsDisposed) + { + mIsDisposed = true; + + FilterDisposed(); + // ParametersDisposed(); + } + } } } diff --git a/src/ActionFilterAttribute.cs b/src/ActionFilterAttribute.cs index 5188823..16315ac 100644 --- a/src/ActionFilterAttribute.cs +++ b/src/ActionFilterAttribute.cs @@ -7,6 +7,11 @@ namespace BeetleX.FastHttpApi [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class FilterAttribute : Attribute { + public virtual void Init(IHttpContext context, ActionHandler handler) + { + + } + public virtual bool Executing(ActionContext context) { return true; @@ -16,6 +21,11 @@ public virtual void Executed(ActionContext context) { } + + public virtual void Disposed(ActionContext context) + { + + } } public class DefaultJsonResultFilter : FilterAttribute @@ -23,7 +33,7 @@ public class DefaultJsonResultFilter : FilterAttribute public override void Executed(ActionContext context) { base.Executed(context); - if (!(context.Result is JsonResult)) + if (!(context.Result is IResult)) context.Result = new JsonResult(context.Result); } } diff --git a/src/ActionHandler.cs b/src/ActionHandler.cs index 234e561..1e73160 100644 --- a/src/ActionHandler.cs +++ b/src/ActionHandler.cs @@ -38,7 +38,7 @@ public ActionHandler(object controller, System.Reflection.MethodInfo method, Htt this.Version = aname.Version.ToString(); Async = false; ControllerUID = AssmblyName + "_" + ControllerType.Name; - + } public ThreadQueueAttribute ThreadQueue { get; set; } @@ -153,12 +153,7 @@ private void LoadParameter() { ParameterBinder pb = new DefaultParameter(); - ParameterBinder[] customPB = (ParameterBinder[])pi.GetCustomAttributes(typeof(ParameterBinder), false); - if (customPB != null && customPB.Length > 0) - { - pb = customPB[0]; - } - else if (pi.ParameterType == typeof(Boolean)) + if (pi.ParameterType == typeof(Boolean)) { pb = new BooleanParameter(); } @@ -261,7 +256,7 @@ private void LoadParameter() public List Parameters { get; private set; } - public object[] GetParameters(IHttpContext context) + public object[] GetParameters(IHttpContext context, ActionContext actionContext) { int count = this.Parameters.Count; object[] parameters = new object[count]; @@ -269,7 +264,21 @@ public object[] GetParameters(IHttpContext context) { try { - parameters[i] = Parameters[i].GetValue(context); + object data = null; + if (this.Parameters[i].Type == typeof(ActionContext)) + { + data = actionContext; + } + else + { + data = Parameters[i].GetValue(context); + } + if (data != null && data is IActionParameter actionContextParameter) + { + actionContextParameter.Context = actionContext; + actionContextParameter.Init(context); + } + parameters[i] = data; } catch (Exception e_) { @@ -356,9 +365,9 @@ public override object GetValue(IHttpContext context) [AttributeUsage(AttributeTargets.Class)] - public class PMapper : Attribute + public class ParameterObjectMapper : Attribute { - public PMapper(Type type) + public ParameterObjectMapper(Type type) { ParameterType = type; } @@ -372,8 +381,7 @@ public interface IParameterBinder object GetValue(IHttpContext context); } - [AttributeUsage(AttributeTargets.Parameter)] - public abstract class ParameterBinder : Attribute, IParameterBinder + public abstract class ParameterBinder :IParameterBinder { public Type Type { get; internal set; } diff --git a/src/ActionHandlerFactory.cs b/src/ActionHandlerFactory.cs index 271c091..18e6511 100644 --- a/src/ActionHandlerFactory.cs +++ b/src/ActionHandlerFactory.cs @@ -1,8 +1,10 @@ using BeetleX.FastHttpApi.Data; using BeetleX.FastHttpApi.WebSockets; +using BeetleX.Tracks; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -20,7 +22,7 @@ public ActionHandlerFactory(HttpApiServer server) private System.Collections.Generic.Dictionary mMethods = new Dictionary(StringComparer.OrdinalIgnoreCase); - private Dictionary mParameterBinders = new Dictionary(); + private Dictionary mParameterBinders = new Dictionary(); public Action AssembliesLoading { get; set; } @@ -32,7 +34,7 @@ public void Register(params Assembly[] assemblies) Type[] types = item.GetTypes(); foreach (Type type in types) { - PMapper mapper = type.GetCustomAttribute(false); + ParameterObjectMapper mapper = type.GetCustomAttribute(false); if (mapper != null) { RegisterParameterBinder(mapper.ParameterType, type); @@ -66,7 +68,7 @@ public void Register(params Assembly[] assemblies) if (Server.EnableLog(EventArgs.LogType.Error)) { string msg = $"{type} controller register error {e_.Message} {e_.StackTrace}"; - Server.Log(EventArgs.LogType.Error, msg); + Server.Log(EventArgs.LogType.Error, null, msg); } } } @@ -75,19 +77,24 @@ public void Register(params Assembly[] assemblies) } } + public void RegisterParameterBinder(Type type, ParameterBinder binder) + { + mParameterBinders[type] = binder; + if (Server.EnableLog(EventArgs.LogType.Info)) + Server.Log(EventArgs.LogType.Info, null, $"Register {type.Name}'s {binder.GetType().Name} parameter binder success"); + } + public void RegisterParameterBinder(Type type, Type binderType) { try { ParameterBinder parameterBinder = (ParameterBinder)Activator.CreateInstance(binderType); - mParameterBinders[type] = binderType; - if (Server.EnableLog(EventArgs.LogType.Info)) - Server.Log(EventArgs.LogType.Info, $"Register {type.Name}'s {binderType.Name} parameter binder success"); + RegisterParameterBinder(type, parameterBinder); } catch (Exception e_) { if (Server.EnableLog(EventArgs.LogType.Error)) - Server.Log(EventArgs.LogType.Error, $"Register {type.Name}'s {binderType.Name} parameter binder error {e_.Message} {e_.StackTrace}"); + Server.Log(EventArgs.LogType.Error, null, $"Register {type.Name}'s {binderType.Name} parameter binder error {e_.Message} {e_.StackTrace}"); } } @@ -99,9 +106,9 @@ public void RegisterParameterBinder(Type type, Type binderType) public ParameterBinder GetParameterBinder(Type type) { - if (mParameterBinders.TryGetValue(type, out Type binderType)) + if (mParameterBinders.TryGetValue(type, out ParameterBinder result)) { - return (ParameterBinder)Activator.CreateInstance(binderType); + return result; } return null; } @@ -176,7 +183,7 @@ public void Remove(ActionHandler handler) mMethods.Remove(handler.Url); if (Server.EnableLog(EventArgs.LogType.Info)) { - Server.Log(EventArgs.LogType.Info, $"remove {handler.Url} action handler"); + Server.Log(EventArgs.LogType.Info, null, $"remove {handler.Url} action handler"); } } @@ -232,7 +239,7 @@ public void Register(Type type, ControllerAttribute ca, Action()) + { + server.UrlRewrite.Add(item.Url, url); + } ActionHandler handler = GetAction(url); if (handler != null) { if (server.EnableLog(EventArgs.LogType.Error)) { - server.Log(EventArgs.LogType.Error, $"{url} already exists! replaced with {controllerType.Name}.{mi.Name}!"); + server.Log(EventArgs.LogType.Error, null, $"{url} already exists! replaced with {controllerType.Name}.{mi.Name}!"); } } handler = new ActionHandler(obj, mi, this.Server); @@ -459,14 +470,14 @@ private void Register(HttpOptions config, Type controllerType, object controller server.ActionSettings(handler); if (server.EnableLog(EventArgs.LogType.Info)) { - server.Log(EventArgs.LogType.Info, $"register { controllerType.Name}.{mi.Name} to [{handler.Method}:{url}]"); + server.Log(EventArgs.LogType.Info, null, $"register { controllerType.Name}.{mi.Name} to [{handler.Method}:{url}]"); } } else { if (server.EnableLog(EventArgs.LogType.Info)) { - server.Log(EventArgs.LogType.Info, $"register { controllerType.Name}.{mi.Name} cancel "); + server.Log(EventArgs.LogType.Info, null, $"register { controllerType.Name}.{mi.Name} cancel "); } } } @@ -479,7 +490,7 @@ private ActionHandler GetAction(string url) return result; } - public void ExecuteWithWS(HttpRequest request, HttpApiServer server, JToken token) + public async Task ExecuteWS(HttpRequest request, HttpApiServer server, JToken token) { ActionResult result = new ActionResult(); JToken url = token["url"]; @@ -487,7 +498,7 @@ public void ExecuteWithWS(HttpRequest request, HttpApiServer server, JToken toke if (url == null) { if (server.EnableLog(EventArgs.LogType.Warring)) - server.BaseServer.Log(EventArgs.LogType.Warring, null, $"Websocket {request.ID} {request.RemoteIPAddress} process error action url info notfound!"); + server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"Websocket {request.ID} {request.RemoteIPAddress} process error action url info notfound!"); result.Code = 403; result.Error = "not support, url info notfound!"; request.Session.Send(dataFrame); @@ -495,8 +506,6 @@ public void ExecuteWithWS(HttpRequest request, HttpApiServer server, JToken toke } result.Url = url.Value(); string baseurl = result.Url; - //if (server.Options.UrlIgnoreCase) - // baseurl = HttpParse.CharToLower(result.Url); if (baseurl[0] != '/') baseurl = "/" + baseurl; result.Url = baseurl; @@ -511,57 +520,86 @@ public void ExecuteWithWS(HttpRequest request, HttpApiServer server, JToken toke if (handler == null) { if (server.EnableLog(EventArgs.LogType.Warring)) - server.BaseServer.Log(EventArgs.LogType.Warring, null, $"Websocket {request.ID} {request.RemoteIPAddress} ws execute {result.Url} notfound!"); + server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"Websocket {request.ID} {request.RemoteIPAddress} ws execute {result.Url} notfound!"); result.Code = 404; result.Error = "url " + baseurl + " notfound!"; request.Session.Send(dataFrame); } else { - try + string parentID = null; + var parentIDToken = data[HttpApiServer.CODE_TREAK_PARENTID]; + if (parentIDToken != null) + parentID = parentIDToken.Value(); + using (var track = CodeTrackFactory.TrackReport(baseurl, CodeTrackLevel.Module, parentID, "WS", "Action")) { - Data.DataContxt dataContxt = new Data.DataContxt(); - DataContextBind.BindJson(dataContxt, data); - WebsocketJsonContext dc = new WebsocketJsonContext(server, request, dataContxt); - dc.ActionUrl = baseurl; - dc.RequestID = result.ID; - if (!Server.OnActionExecuting(dc,handler)) - return; - ActionContext context = new ActionContext(handler, dc, this); - long startTime = server.BaseServer.GetRunTime(); - WSActionResultHandler wSActionResultHandler = new WSActionResultHandler(dc, server, request, result, dataFrame, startTime); - if (!handler.HasValidation || handler.ValidateParamters(context.Parameters, out (Validations.ValidationBase, ParameterInfo) error)) + if (track.Enabled) { - context.Execute(wSActionResultHandler); + Activity.Current.AddTag("ClientIP", request.Session.RemoteEndPoint.ToString()); + Activity.Current?.AddTag("tag", $"Beetlex FastHttpApi"); } - else + ActionContext context = null; + try { - server.ValidationOutputHandler.Execute(dc, wSActionResultHandler, error.Item1, error.Item2); + Data.DataContxt dataContxt = new Data.DataContxt(); + DataContextBind.BindJson(dataContxt, data); + WebsocketJsonContext dc = new WebsocketJsonContext(server, request, dataContxt); + dc.ActionUrl = baseurl; + dc.RequestID = result.ID; + if (Server.OnActionExecuting(dc, handler)) + { + context = new ActionContext(handler, dc, this); + context.Init(); + long startTime = server.BaseServer.GetRunTime(); + WSActionResultHandler wSActionResultHandler = new WSActionResultHandler(dc, server, request, result, dataFrame, startTime); + wSActionResultHandler.ActionHandler = handler; + if (!handler.HasValidation || handler.ValidateParamters(context.Parameters, out (Validations.ValidationBase, ParameterInfo) error)) + { + await context.Execute(wSActionResultHandler); + } + else + { + server.ValidationOutputHandler.Execute(dc, wSActionResultHandler, error.Item1, error.Item2); + } + } + } + catch (Exception e_) + { + handler.IncrementError(); + if (server.EnableLog(EventArgs.LogType.Error)) + server.BaseServer.Log(EventArgs.LogType.Error, request.Session, $"Websocket {request.ID} {request.RemoteIPAddress} execute {result.Url} inner error {e_.Message}@{e_.StackTrace}"); + result.Code = 500; + result.Error = e_.Message; + if (server.Options.OutputStackTrace) + { + result.StackTrace = e_.StackTrace; + } + dataFrame.Send(request.Session, true); + context?.Dispose(); } } - catch (Exception e_) + if (Server.EnableLog(EventArgs.LogType.Debug)) { - handler.IncrementError(); - if (server.EnableLog(EventArgs.LogType.Error)) - server.BaseServer.Log(EventArgs.LogType.Error, null, $"Websocket {request.ID} {request.RemoteIPAddress} execute {result.Url} inner error {e_.Message}@{e_.StackTrace}"); - result.Code = 500; - result.Error = e_.Message; - if (server.Options.OutputStackTrace) + if (CodeTrackFactory.Activity != null) { - result.StackTrace = e_.StackTrace; + Server.Log(EventArgs.LogType.Debug, request.Session, $"Websocket {request.ID} {request.RemoteIPAddress} execute {result.Url} {CodeTrackFactory.Activity.GetReport()}"); } - dataFrame.Send(request.Session, true); } } } - public void Execute(HttpRequest request, HttpResponse response, HttpApiServer server) + public async Task Execute(HttpRequest request, HttpResponse response, HttpApiServer server) { - ActionHandler handler = GetAction(request.BaseUrl); + string actionUrl = request.BaseUrl; + if (!string.IsNullOrEmpty(request.Ext)) + { + actionUrl = request.Path + System.IO.Path.GetFileNameWithoutExtension(actionUrl); + } + ActionHandler handler = GetAction(actionUrl); if (handler == null) { if (server.EnableLog(EventArgs.LogType.Warring)) - server.BaseServer.Log(EventArgs.LogType.Warring, null, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {request.Url} not found"); + server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {request.Url} not found"); if (!server.OnHttpRequesNotfound(request, response).Cancel) { NotFoundResult notFoundResult = new NotFoundResult($"{request.Method} {request.Url} not found"); @@ -570,68 +608,88 @@ public void Execute(HttpRequest request, HttpResponse response, HttpApiServer se } else { - try + string parentID = request.TrackParentID; + using (var track = CodeTrackFactory.TrackReport(actionUrl, CodeTrackLevel.Module, parentID, "HTTP", "Action")) { - if (handler.Method.IndexOf(request.Method, StringComparison.OrdinalIgnoreCase) == -1) + if (track.Enabled) { - if (request.Method == HttpParse.OPTIONS_TAG && handler.OptionsAttribute != null) - { - if (server.EnableLog(EventArgs.LogType.Info)) - server.BaseServer.Log(EventArgs.LogType.Info, null, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {request.Url} request"); - response.Result(handler.OptionsAttribute); - } - else - { - if (server.EnableLog(EventArgs.LogType.Warring)) - server.BaseServer.Log(EventArgs.LogType.Warring, null, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {request.Url} not support"); - BadRequestResult notSupportResult = new BadRequestResult($"{request.Method}{request.Url} not support"); - response.Result(notSupportResult); - } - return; + Activity.Current?.AddTag("ClientIP", request.Session.RemoteEndPoint.ToString()); + Activity.Current?.AddTag("tag", $"Beetlex FastHttpApi"); } - request.ActionHandler = handler; - DataConvertAttribute dataConverter = null; - if (!handler.NoConvert) + ActionContext context = null; + try { - if (Server.Options.FixedConverter) + if (handler.Method.IndexOf(request.Method, StringComparison.OrdinalIgnoreCase) == -1) { - if (handler.DataConverter == null) - handler.DataConverter = DataContextBind.GetConvertAttribute(request.ContentType); - dataConverter = handler.DataConverter; + if (request.Method == HttpParse.OPTIONS_TAG && handler.OptionsAttribute != null) + { + if (server.EnableLog(EventArgs.LogType.Info)) + server.BaseServer.Log(EventArgs.LogType.Info, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {request.Url} request"); + response.Result(handler.OptionsAttribute); + } + else + { + if (server.EnableLog(EventArgs.LogType.Warring)) + server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {request.Url} not support"); + BadRequestResult notSupportResult = new BadRequestResult($"{request.Method}{request.Url} not support"); + response.Result(notSupportResult); + } + return; } - else + request.ActionHandler = handler; + DataConvertAttribute dataConverter = null; + if (!handler.NoConvert) { - dataConverter = DataContextBind.GetConvertAttribute(request.ContentType); + if (Server.Options.FixedConverter) + { + if (handler.DataConverter == null) + handler.DataConverter = DataContextBind.GetConvertAttribute(request.ContentType); + dataConverter = handler.DataConverter; + } + else + { + dataConverter = DataContextBind.GetConvertAttribute(request.ContentType); + } + } + if (dataConverter != null) + dataConverter.Execute(request.Data, request); + HttpContext pc = new HttpContext(server, request, response, request.Data); + long startTime = server.BaseServer.GetRunTime(); + pc.ActionUrl = request.BaseUrl; + if (Server.OnActionExecuting(pc, handler)) + { + HttpActionResultHandler actionResult = new HttpActionResultHandler(pc, Server, request, response, startTime); + context = new ActionContext(handler, pc, this); + context.Init(); + if (handler.OptionsAttribute != null) + handler.OptionsAttribute.SetResponse(request, response); + if (!handler.HasValidation || handler.ValidateParamters(context.Parameters, out (Validations.ValidationBase, ParameterInfo) error)) + { + await context.Execute(actionResult); + } + else + { + server.ValidationOutputHandler.Execute(pc, actionResult, error.Item1, error.Item2); + } } } - if (dataConverter != null) - dataConverter.Execute(request.Data, request); - HttpContext pc = new HttpContext(server, request, response, request.Data); - long startTime = server.BaseServer.GetRunTime(); - pc.ActionUrl = request.BaseUrl; - if (!Server.OnActionExecuting(pc,handler)) - return; - HttpActionResultHandler actionResult = new HttpActionResultHandler(Server, request, response, startTime); - ActionContext context = new ActionContext(handler, pc, this); - if (handler.OptionsAttribute != null) - handler.OptionsAttribute.SetResponse(request, response); - if (!handler.HasValidation || handler.ValidateParamters(context.Parameters, out (Validations.ValidationBase, ParameterInfo) error)) - { - context.Execute(actionResult); - } - else + catch (Exception e_) { - server.ValidationOutputHandler.Execute(pc, actionResult, error.Item1, error.Item2); + handler.IncrementError(); + if (server.EnableLog(EventArgs.LogType.Error)) + server.Log(EventArgs.LogType.Error, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} { request.Url} inner error {e_.Message}@{e_.StackTrace}"); + //InnerErrorResult result = new InnerErrorResult($"http execute {request.BaseUrl} error ", e_, server.Options.OutputStackTrace); + //response.Result(result); + response.InnerError($"http execute {request.BaseUrl} inner error!", e_, server.Options.OutputStackTrace); + context?.Dispose(); } } - catch (Exception e_) + if (Server.EnableLog(EventArgs.LogType.Debug)) { - handler.IncrementError(); - if (server.EnableLog(EventArgs.LogType.Error)) - server.Log(EventArgs.LogType.Error, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} { request.Url} inner error {e_.Message}@{e_.StackTrace}"); - InnerErrorResult result = new InnerErrorResult($"http execute {request.BaseUrl} error ", e_, server.Options.OutputStackTrace); - response.Result(result); - + if (CodeTrackFactory.Activity != null) + { + Server.Log(EventArgs.LogType.Debug, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} { request.Url} {CodeTrackFactory.Activity.GetReport()}"); + } } } } diff --git a/src/ActionSettings.cs b/src/ActionSettings.cs index 8e2ea3e..d0feb74 100644 --- a/src/ActionSettings.cs +++ b/src/ActionSettings.cs @@ -14,7 +14,6 @@ public ActionInfo() { } - public ActionInfo(ActionHandler handler) { Url = handler.Url; @@ -24,7 +23,6 @@ public ActionInfo(ActionHandler handler) else ThreadInfo = new ThreadInfo(); } - public string Url { get; set; } public int MaxRps { get; set; } @@ -43,7 +41,6 @@ public void SetTo(ActionHandler handler) handler.ThreadQueue = ThreadInfo?.GetThreadQueue(); } } - public override string ToString() { return $"{Url} {MaxRps} {ThreadInfo}"; diff --git a/src/AutoLoaderAttribute.cs b/src/AutoLoaderAttribute.cs new file mode 100644 index 0000000..5dc063f --- /dev/null +++ b/src/AutoLoaderAttribute.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + [AttributeUsage(AttributeTargets.Assembly, Inherited = false)] + public class AssemblyAutoLoaderAttribute : Attribute + { + public AssemblyAutoLoaderAttribute(string id) + { + + } + } +} diff --git a/src/BeetleX.FastHttpApi.csproj b/src/BeetleX.FastHttpApi.csproj index 3f6bec0..5b70a0e 100644 --- a/src/BeetleX.FastHttpApi.csproj +++ b/src/BeetleX.FastHttpApi.csproj @@ -1,25 +1,25 @@  - netcoreapp2.1 - Copyright © 2019-2020 beetlex.io + netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 + Copyright © beetlex-io.com 2019-2022 email: admin@beetlex-io.com or henryfan@msn.com henryfan beetlex.io high performance and lightweight http and websocket server components for .NETCore - 1.8.2.7 - https://github.com/IKende/FastHttpApi - https://github.com/IKende/FastHttpApi/blob/master/LICENSE - 1.8.2.7 - 1.8.2.7 + 2.2.24.0425 + https://github.com/beetlex-io/FastHttpApi + + 2.2.24.0425 + 2.2.24.0425 7.3 BeetleX.FastHttpApi - false high performance and lightweight http and websocket server components for .NETCore - true + False Beetlex.FastHttpApi.pfx false - beetlex200.png + + E:\public @@ -31,8 +31,11 @@ + + + @@ -44,10 +47,6 @@ - - True - - @@ -55,8 +54,12 @@ - + + + + + diff --git a/src/BeetleX.FastHttpApi.csproj.user b/src/BeetleX.FastHttpApi.csproj.user index 82904d4..df5f05d 100644 --- a/src/BeetleX.FastHttpApi.csproj.user +++ b/src/BeetleX.FastHttpApi.csproj.user @@ -1,7 +1,7 @@  - <_LastSelectedProfileId>I:\VisualStudio\BeetleX\BeetleX\BeetleX.FastHttpAPI\Properties\PublishProfiles\FolderProfile.pubxml + <_LastSelectedProfileId>D:\beetlexproject\BeetleX\BeetleX\BeetleX.FastHttpAPI\Properties\PublishProfiles\FolderProfile.pubxml false \ No newline at end of file diff --git a/src/Clients/ClientActionHanler.cs b/src/Clients/ClientActionHanler.cs new file mode 100644 index 0000000..90c6ffd --- /dev/null +++ b/src/Clients/ClientActionHanler.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using static BeetleX.FastHttpApi.RouteTemplateMatch; + +namespace BeetleX.FastHttpApi.Clients +{ + class ClientActionHanler + { + + private List mRouteParameters = new List(); + + private List mHeaderParameters = new List(); + + private List mQueryStringParameters = new List(); + + private List mDataParameters = new List(); + + public string Method { get; set; } + + public string Name { get; set; } + + public string BaseUrl { get; set; } + + private Dictionary mHeaders = new Dictionary(); + + private Dictionary mQueryString = new Dictionary(); + + public ControllerAttribute Controller { get; set; } + + public IClientBodyFormater Formater { get; set; } + + public MethodInfo MethodInfo { get; set; } + + public RouteTemplateMatch RouteTemplateMatch { get; set; } + + public Type DeclaringType + { get; set; } + + public bool Async { get; set; } + + public Type ReturnType { get; set; } + + public Type MethodType { get; set; } + + public ClientActionHanler(MethodInfo method) + { + MethodInfo = method; + Method = "GET"; + Name = method.Name; + DeclaringType = method.DeclaringType; + MethodType = MethodInfo.ReturnType; + Async = false; + if (MethodInfo.ReturnType != typeof(void)) + { + if (MethodInfo.ReturnType.Name == "Task`1" || MethodInfo.ReturnType.Name == "ValueTask`1" || MethodInfo.ReturnType == typeof(ValueTask)) + { + Async = true; + if (MethodInfo.ReturnType.IsGenericType) + ReturnType = MethodInfo.ReturnType.GetGenericArguments()[0]; + } + else + { + + ReturnType = MethodInfo.ReturnType; + } + } + foreach (CHeaderAttribute h in DeclaringType.GetCustomAttributes()) + { + if (!string.IsNullOrEmpty(h.Name) && !string.IsNullOrEmpty(h.Value)) + { + mHeaders[h.Name] = h.Value; + } + } + + foreach (CHeaderAttribute h in method.GetCustomAttributes()) + { + if (!string.IsNullOrEmpty(h.Name) && !string.IsNullOrEmpty(h.Value)) + { + mHeaders[h.Name] = h.Value; + } + } + + foreach (CQueryAttribute q in DeclaringType.GetCustomAttributes()) + { + if (!string.IsNullOrEmpty(q.Name) && !string.IsNullOrEmpty(q.Value)) + { + mQueryString[q.Name] = q.Value; + } + } + + foreach (CQueryAttribute q in method.GetCustomAttributes()) + { + if (!string.IsNullOrEmpty(q.Name) && !string.IsNullOrEmpty(q.Value)) + { + mQueryString[q.Name] = q.Value; + } + } + + Formater = method.GetCustomAttribute(); + if (Formater == null) + Formater = DeclaringType.GetCustomAttribute(); + var get = method.GetCustomAttribute(); + if (get != null) + { + Method = Request.GET; + if (!string.IsNullOrEmpty(get.Route)) + RouteTemplateMatch = new RouteTemplateMatch(get.Route); + } + var post = method.GetCustomAttribute(); + if (post != null) + { + Method = Request.POST; + if (!string.IsNullOrEmpty(post.Route)) + RouteTemplateMatch = new RouteTemplateMatch(post.Route); + } + var del = method.GetCustomAttribute(); + if (del != null) + { + Method = Request.DELETE; + if (!string.IsNullOrEmpty(del.Route)) + RouteTemplateMatch = new RouteTemplateMatch(del.Route); + } + var put = method.GetCustomAttribute(); + if (put != null) + { + Method = Request.PUT; + if (!string.IsNullOrEmpty(put.Route)) + RouteTemplateMatch = new RouteTemplateMatch(put.Route); + } + Controller = this.DeclaringType.GetCustomAttribute(); + if (Controller != null) + { + if (!string.IsNullOrEmpty(Controller.BaseUrl)) + BaseUrl = Controller.BaseUrl; + } + if (string.IsNullOrEmpty(BaseUrl)) + BaseUrl = "/"; + if (BaseUrl[0] != '/') + BaseUrl = "/" + BaseUrl; + if (BaseUrl.Substring(BaseUrl.Length - 1, 1) != "/") + BaseUrl += "/"; + int index = 0; + foreach (var p in method.GetParameters()) + { + ClientActionParameter cap = new ClientActionParameter(); + cap.Name = p.Name; + cap.ParameterType = p.ParameterType; + cap.Index = index; + index++; + CHeaderAttribute cHeader = p.GetCustomAttribute(); + if (cHeader != null) + { + if (!string.IsNullOrEmpty(cHeader.Name)) + cap.Name = cHeader.Name; + mHeaderParameters.Add(cap); + } + else + { + CQueryAttribute cQuery = p.GetCustomAttribute(); + if (cQuery != null) + { + if (!string.IsNullOrEmpty(cQuery.Name)) + cap.Name = cQuery.Name; + mQueryStringParameters.Add(cap); + } + else + { + if (RouteTemplateMatch != null && RouteTemplateMatch.Items.Find(i => i.Name == p.Name) != null) + { + mRouteParameters.Add(cap); + } + else + { + mDataParameters.Add(cap); + } + } + } + } + } + + public RequestInfo GetRequestInfo(object[] parameters) + { + RequestInfo result = new RequestInfo(); + if (mHeaders.Count > 0) + { + if (result.Header == null) + result.Header = new Dictionary(); + foreach (var kv in mHeaders) + result.Header[kv.Key] = kv.Value; + } + if (mQueryString.Count > 0) + { + if (result.QueryString == null) + result.QueryString = new Dictionary(); + foreach (var kv in mQueryString) + result.QueryString[kv.Key] = kv.Value; + } + result.Method = this.Method; + StringBuilder sb = new StringBuilder(); + sb.Append(BaseUrl); + if (RouteTemplateMatch != null) + { + if (RouteTemplateMatch.Items.Count > 0) + { + List items = RouteTemplateMatch.Items; + for (int i = 0; i < items.Count; i++) + { + var item = items[i]; + if (!string.IsNullOrEmpty(item.Start)) + sb.Append(item.Start); + ClientActionParameter cap = mRouteParameters.Find(p => p.Name == item.Name); + if (cap != null) + sb.Append(parameters[cap.Index]); + + if (!string.IsNullOrEmpty(item.Eof)) + sb.Append(item.Eof); + } + } + else + { + sb.Append(RouteTemplateMatch.Template); + } + } + else + { + sb.Append(MethodInfo.Name); + } + if (mDataParameters.Count > 0) + { + if (Method == Request.DELETE || Method == Request.GET) + { + if (result.QueryString == null) + result.QueryString = new Dictionary(); + foreach (var item in mDataParameters) + { + if (parameters[item.Index] != null) + result.QueryString[item.Name] = parameters[item.Index].ToString(); + } + } + else + { + var data = new Dictionary(); + foreach (var item in mDataParameters) + { + if (parameters[item.Index] != null) + data[item.Name] = parameters[item.Index]; + } + result.Data = data; + } + } + if (mHeaderParameters.Count > 0) + { + if (result.Header == null) + result.Header = new Dictionary(); + foreach (var item in mHeaderParameters) + { + if (parameters[item.Index] != null) + result.Header[item.Name] = parameters[item.Index].ToString(); + } + } + if (mQueryStringParameters.Count > 0) + { + if (result.QueryString == null) + result.QueryString = new Dictionary(); + foreach (var item in mQueryStringParameters) + { + if (parameters[item.Index] != null) + result.QueryString[item.Name] = parameters[item.Index].ToString(); + } + } + result.Type = ReturnType; + result.Url = sb.ToString(); + result.Formatter = this.Formater; + return result; + } + + public struct RequestInfo + { + public string Method; + + public Type Type; + + public string Url; + + public IClientBodyFormater Formatter; + + public Dictionary Data; + + public Dictionary Header; + + public Dictionary QueryString; + + public Request GetRequest(HttpHost httpApiClient) + { + switch (Method) + { + case Request.POST: + return httpApiClient.Post(Url, Header, QueryString, Data, Formatter, Type); + case Request.PUT: + return httpApiClient.Put(Url, Header, QueryString, Data, Formatter, Type); + case Request.DELETE: + return httpApiClient.Delete(Url, Header, QueryString, Formatter, Type); + default: + return httpApiClient.Get(Url, Header, QueryString, Formatter, Type); + } + } + } + + public ApiNodeAgent NodeAgent { get; set; } + + } + + class ClientActionParameter + { + + public long Version { get; set; } + + public string Name { get; set; } + + public Type ParameterType { get; set; } + + public int Index { get; set; } + + } + + class ClientActionFactory + { + static System.Collections.Concurrent.ConcurrentDictionary mHandlers = new System.Collections.Concurrent.ConcurrentDictionary(); + + public static ClientActionHanler GetHandler(MethodInfo method) + { + ClientActionHanler result; + if (!mHandlers.TryGetValue(method, out result)) + { + result = new ClientActionHanler(method); + mHandlers[method] = result; + } + return result; + } + } +} diff --git a/src/Clients/HttpApiBase.cs b/src/Clients/HttpApiBase.cs new file mode 100644 index 0000000..06cce69 --- /dev/null +++ b/src/Clients/HttpApiBase.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace BeetleX.FastHttpApi.Clients +{ + + public class HttpApiProxy : System.Reflection.DispatchProxy + { + public HttpApiProxy() + { + TimeOut = 10000; + } + + public HttpHost Host { get; set; } + + public int TimeOut { get; set; } + + protected override object Invoke(MethodInfo targetMethod, object[] args) + { + ClientActionHanler handler = ClientActionFactory.GetHandler((MethodInfo)targetMethod); + var rinfo = handler.GetRequestInfo(args); + var request = rinfo.GetRequest(Host); + var task = request.Execute(); + if (!handler.Async) + { + task.Wait(TimeOut); + if (!task.Wait(TimeOut)) + { + throw new HttpClientException(request, Host.Uri, $"{rinfo.Method} {rinfo.Url} request time out!"); + } + if (task.Result.Exception != null) + throw task.Result.Exception; + return task.Result.Body; + } + else + { + if (handler.MethodType == typeof(ValueTask)) + { + AnyCompletionSource source = new AnyCompletionSource(); + source.WaitResponse(task); + return new ValueTask(source.Task); + } + else + { + Type gtype = typeof(AnyCompletionSource<>); + Type type = gtype.MakeGenericType(handler.ReturnType); + IAnyCompletionSource source = (IAnyCompletionSource)Activator.CreateInstance(type); + source.WaitResponse(task); + return source.GetTask(); + } + } + } + } + + interface IAnyCompletionSource + { + void Success(object data); + void Error(Exception error); + void WaitResponse(Task task); + Task GetTask(); + } + + class AnyCompletionSource : TaskCompletionSource, IAnyCompletionSource + { + public void Success(object data) + { + TrySetResult((T)data); + } + + public void Error(Exception error) + { + TrySetException(error); + } + + public async void WaitResponse(Task task) + { + var response = await task; + if (response.Exception != null) + Error(response.Exception); + else + Success(response.Body); + } + + public Task GetTask() + { + return this.Task; + } + } + + public class HttpApiClient + { + public HttpApiClient(string host) + { + Host = new HttpHost(host); + } + + public HttpHost Host { get; set; } + + protected async Task OnExecute(MethodBase targetMethod, params object[] args) + { + var rinfo = ClientActionFactory.GetHandler((MethodInfo)targetMethod).GetRequestInfo(args); + var request = rinfo.GetRequest(Host); + var respnse = await request.Execute(); + if (respnse.Exception != null) + throw respnse.Exception; + return (T)respnse.Body; + } + + private System.Collections.Concurrent.ConcurrentDictionary mAPI = new System.Collections.Concurrent.ConcurrentDictionary(); + + public T Create() + { + Type type = typeof(T); + object result; + if (!mAPI.TryGetValue(type, out result)) + { + result = DispatchProxy.Create(); + mAPI[type] = result; + ((HttpApiProxy)result).Host = Host; + } + return (T)result; + } + } +} diff --git a/src/Clients/HttpClient.cs b/src/Clients/HttpClient.cs new file mode 100644 index 0000000..5200880 --- /dev/null +++ b/src/Clients/HttpClient.cs @@ -0,0 +1,405 @@ +using BeetleX.Clients; +using BeetleX.FastHttpApi.Clients; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace BeetleX.FastHttpApi +{ + + public class HttpClientPool + { + public HttpClientPool(string host, int port, bool ssl = false) + { + Host = host; + Port = port; + TimeOut = 10000; + Async = true; + MaxConnections = 100; + Clients = new List(); + SSL = ssl; + } + + public bool SSL { get; set; } + + public string Host { get; set; } + + public int Port { get; set; } + + private System.Collections.Concurrent.ConcurrentQueue mPools = new System.Collections.Concurrent.ConcurrentQueue(); + + public List Clients { get; private set; } + + private int mConnections = 0; + + public int MaxConnections { get; set; } + + public int Connections => mConnections; + + public int TimeOut { get; set; } + + public bool Async { get; set; } + + public HttpClient Pop(bool recursion = false) + { + HttpClient result; + if (!mPools.TryDequeue(out result)) + { + int value = System.Threading.Interlocked.Increment(ref mConnections); + if (value > MaxConnections) + { + System.Threading.Interlocked.Decrement(ref mConnections); + if (recursion) + { + throw new Exception($"Unable to reach {Host}:{Port} HTTP request, exceeding maximum number of connections"); + } + else + { + for (int i = 0; i < Clients.Count; i++) + { + HttpClient client = Clients[i]; + if (client.IsTimeout && client.Using) + { + Task.Run(() => + { + try + { + client.RequestCommpletionSource.Error(new TimeoutException($"{Host}:{Port} request timeout!")); + } + finally + { + client.Client.DisConnect(); + } + }); + } + } + System.Threading.Thread.Sleep(50); + return Pop(true); + } + } + var packet = new HttpClientPacket(); + if (Async) + { + AsyncTcpClient client; + if (SSL) + { + client = SocketFactory.CreateSslClient(packet, Host, Port, Host); + } + else + { + client = SocketFactory.CreateClient(packet, Host, Port); + } + packet.Client = client; + client.Connected = c => + { + c.Socket.NoDelay = true; + c.Socket.ReceiveTimeout = TimeOut; + c.Socket.SendTimeout = TimeOut; + }; + result = new HttpClient(); + result.Client = client; + result.Node = new LinkedListNode(result); + Clients.Add(result); + } + else + { + TcpClient client; + if (SSL) + { + client = SocketFactory.CreateSslClient(packet, Host, Port, Host); + } + else + { + client = SocketFactory.CreateClient(packet, Host, Port); + } + packet.Client = client; + client.Connected = c => + { + c.Socket.NoDelay = true; + c.Socket.ReceiveTimeout = TimeOut; + c.Socket.SendTimeout = TimeOut; + }; + result = new HttpClient(); + result.Client = client; + result.Node = new LinkedListNode(result); + Clients.Add(result); + } + } + result.Using = true; + result.TimeOut = BeetleX.TimeWatch.GetElapsedMilliseconds() + TimeOut * 1000; + return result; + } + + public void Push(HttpClient client) + { + client.Using = false; + mPools.Enqueue(client); + } + } + + public class HttpClient + { + public IClient Client { get; set; } + + public LinkedListNode Node { get; set; } + + public HttpClientPool Pool { get; set; } + + internal long TimeOut { get; set; } + + public bool Using { get; set; } + + public bool IsTimeout + { + get + { + return BeetleX.TimeWatch.GetElapsedMilliseconds() > TimeOut; + } + } + + internal IAnyCompletionSource RequestCommpletionSource { get; set; } + + } + + public class HttpClientPoolFactory + { + + private static System.Collections.Concurrent.ConcurrentDictionary mPools = new System.Collections.Concurrent.ConcurrentDictionary(); + + public static System.Collections.Concurrent.ConcurrentDictionary Pools => mPools; + + public static void SetPoolInfo(Uri host, int maxConn, int timeout, bool async = true) + { + HttpClientPool pool = GetPool(null, host); + pool.MaxConnections = maxConn; + pool.TimeOut = timeout; + pool.Async = async; + } + + public static void SetPoolInfo(string host, int maxConn, int timeout, bool async = true) + { + SetPoolInfo(new Uri(host), maxConn, timeout, async); + } + + public static HttpClientPool GetPool(string key, Uri uri) + { + if (string.IsNullOrEmpty(key)) + key = $"{uri.Host}:{uri.Port}"; + HttpClientPool result; + if (mPools.TryGetValue(key, out result)) + return result; + return CreatePool(key, uri); + } + + private static HttpClientPool CreatePool(string key, Uri uri) + { + lock (typeof(HttpClientPoolFactory)) + { + HttpClientPool result; + if (!mPools.TryGetValue(key, out result)) + { + bool ssl = uri.Scheme.ToLower() == "https"; + result = new HttpClientPool(uri.Host, uri.Port, ssl); + mPools[key] = result; + } + return result; + } + } + } + + public class HttpHost + { + public HttpHost(string host) : this(new Uri(host)) + { + + } + + public HttpHost(Uri host) + { + this.Uri = host; + Formater = new FormUrlFormater(); + Host = this.Uri.Host; + Port = this.Uri.Port; + mPoolKey = $"{this.Uri.Host}:{this.Uri.Port}"; + mPool = HttpClientPoolFactory.GetPool(mPoolKey, this.Uri); + Available = true; + InVerify = false; + } + + private HttpClientPool mPool; + + public long ID { get; set; } + + private long mSuccess; + + private long mLastSuccess; + + private long mError; + + private int mSocketErrors; + + public int Weight { get; set; } + + public HttpClientPool Pool => mPool; + + private string mPoolKey; + + public static int DisconnectErrors { get; set; } = 5; + + public string Host { get; set; } + + public int Port { get; set; } + + public IClientBodyFormater Formater { get; set; } + + public Uri Uri { get; private set; } + + public bool Available { get; set; } + + public long Success => mSuccess; + + public long Error => mError; + + internal int SocketErrors => mSocketErrors; + + internal bool InVerify { get; set; } + + //private int mRps; + + //private long mLastTime; + + //public int RPS => mRps; + + public int MaxRPS { get; set; } + + //public bool ValidateRps() + //{ + // if (MaxRPS == 0) + // return true; + // long now = TimeWatch.GetElapsedMilliseconds(); + // if (now - mLastTime >= 1000) + // return true; + // return mRps < MaxRPS; + //} + + internal void AddSuccess() + { + mSuccess++; + Available = true; + //long now = TimeWatch.GetElapsedMilliseconds(); + //if (now - mLastTime >= 1000) + //{ + // mRps = 1; + // mLastTime = now; + //} + //else + //{ + // mRps++; + //} + } + + internal void AddError(bool socketError) + { + if (socketError) + { + mSocketErrors++; + + } + else + { + mSocketErrors = 0; + + } + if (mSocketErrors >= DisconnectErrors) + Available = false; + else + Available = true; + mError++; + } + + public override string ToString() + { + string result = $"{mSuccess - mLastSuccess}/{mSuccess}"; + mLastSuccess = mSuccess; + return result; + } + + public Request Put(string url, Dictionary header, Dictionary queryString, Dictionary data, IClientBodyFormater formater, Type bodyType = null) + { + Request request = new Request(); + request.Method = Request.PUT; + request.Formater = formater == null ? this.Formater : formater; + request.Header = new Header(); + request.Header[HeaderTypeFactory.CONTENT_TYPE] = request.Formater.ContentType; + request.Header[HeaderTypeFactory.HOST] = Host; + if (header != null) + foreach (var item in header) + request.Header[item.Key] = item.Value; + request.QuestryString = queryString; + request.Url = url; + request.Body = data; + request.BodyType = bodyType; + request.HttpHost = this; + return request; + } + + public Request Post(string url, Dictionary header, Dictionary queryString, Dictionary data, IClientBodyFormater formater, Type bodyType = null) + { + Request request = new Request(); + request.Method = Request.POST; + request.Formater = formater == null ? this.Formater : formater; + request.Header = new Header(); + + request.Header[HeaderTypeFactory.CONTENT_TYPE] = request.Formater.ContentType; + request.Header[HeaderTypeFactory.HOST] = Host; + if (header != null) + foreach (var item in header) + request.Header[item.Key] = item.Value; + request.QuestryString = queryString; + request.Url = url; + request.Body = data; + request.BodyType = bodyType; + request.HttpHost = this; + return request; + } + + public Request Delete(string url, Dictionary header, Dictionary queryString, IClientBodyFormater formater, Type bodyType = null) + { + Request request = new Request(); + request.Header = new Header(); + request.Formater = formater == null ? this.Formater : formater; + request.Method = Request.DELETE; + request.Header[HeaderTypeFactory.HOST] = Host; + request.QuestryString = queryString; + if (header != null) + foreach (var item in header) + request.Header[item.Key] = item.Value; + request.Url = url; + request.BodyType = bodyType; + request.HttpHost = this; + return request; + } + + public Request Get(string url, Dictionary header, Dictionary queryString, IClientBodyFormater formater, Type bodyType = null) + { + Request request = new Request(); + request.Header = new Header(); + request.Formater = formater == null ? this.Formater : formater; + request.Header[HeaderTypeFactory.CONTENT_TYPE] = request.Formater.ContentType; + request.Header[HeaderTypeFactory.HOST] = Host; + request.QuestryString = queryString; + if (header != null) + foreach (var item in header) + request.Header[item.Key] = item.Value; + request.Url = url; + request.BodyType = bodyType; + request.HttpHost = this; + return request; + } + + } + + +} diff --git a/src/Clients/HttpClientException.cs b/src/Clients/HttpClientException.cs new file mode 100644 index 0000000..dba9ee5 --- /dev/null +++ b/src/Clients/HttpClientException.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi.Clients +{ + public class HttpClientException : Exception + { + public HttpClientException(Request request, Uri host, string message, Exception innerError = null) : base($"request {host} error {message}", innerError) + { + Request = request; + Host = host; + SocketError = false; + if (innerError != null && (innerError is System.Net.Sockets.SocketException || innerError is ObjectDisposedException)) + { + SocketError = true; + } + } + public Uri Host { get; set; } + + public Request Request { get; set; } + + public bool SocketError { get; internal set; } + + } +} diff --git a/src/Clients/HttpClientPacket.cs b/src/Clients/HttpClientPacket.cs new file mode 100644 index 0000000..3c2eb5b --- /dev/null +++ b/src/Clients/HttpClientPacket.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using BeetleX.Buffers; +using BeetleX.Clients; + +namespace BeetleX.FastHttpApi.Clients +{ + public class HttpClientPacket : BeetleX.Clients.IClientPacket + { + public HttpClientPacket() + { + + } + + private Response response; + + public EventClientPacketCompleted Completed { get; set; } + + public IClient Client { get; set; } + + public IClientPacket Clone() + { + if (pipeStream != null) + { + pipeStream.Dispose(); + pipeStream = null; + } + HttpClientPacket result = new HttpClientPacket(); + result.Client = this.Client; + this.Client = null; + return result; + } + + private BeetleX.Buffers.PipeStream pipeStream; + + private int chunkeLength; + + private bool end = false; + + private void loadChunkedData(PipeStream stream) + { + + Next: + string line; + if (chunkeLength > 0) + { + if (pipeStream == null) + pipeStream = new PipeStream(); + while (true) + { + byte[] buffer = HttpParse.GetByteBuffer(); + int count = buffer.Length; + if (count > chunkeLength) + count = chunkeLength; + int read = stream.Read(buffer, 0, count); + if (read == 0) + return; + pipeStream.Write(buffer, 0, read); + chunkeLength -= read; + if (chunkeLength == 0) + { + chunkeLength = 0; + break; + } + } + } + else + { + if (!stream.TryReadWith(HeaderTypeFactory.LINE_BYTES, out line)) + return; + if (string.IsNullOrEmpty(line)) + { + if (end) + { + var item = response; + pipeStream.Flush(); + item.Stream = pipeStream; + response = null; + pipeStream = null; + end = false; + Completed?.Invoke(Client, item); + return; + } + else + { + goto Next; + } + } + else + { + try + { + chunkeLength = int.Parse(line, System.Globalization.NumberStyles.HexNumber); + if (chunkeLength == 0) + { + end = true; + } + else + response.Length += chunkeLength; + } + catch (Exception e_) + { + throw e_; + } + } + } + if (stream.Length > 0) + goto Next; + } + + public void Decode(IClient client, Stream stream) + { + var pipeStream = stream.ToPipeStream(); + if (response == null) + { + response = new Response(); + } + if (response.Load(pipeStream) == LoadedState.Completed) + { + if (response.Chunked) + { + loadChunkedData(pipeStream); + } + else + { + if (response.Length == 0) + { + var item = response; + response = null; + Completed?.Invoke(Client, item); + } + else + { + if (pipeStream.Length >= response.Length) + { + var item = response; + item.Stream = pipeStream; + response = null; + Completed?.Invoke(Client, item); + } + } + } + } + } + + private bool mIsDisposed = false; + + public void Dispose() + { + if (!mIsDisposed) + { + Client = null; + mIsDisposed = true; + if (pipeStream != null) + { + pipeStream.Dispose(); + pipeStream = null; + } + } + } + + public void Encode(object data, IClient client, Stream stream) + { + Request request = (Request)data; + request.Execute(stream.ToPipeStream()); + } + } +} diff --git a/src/Clients/HttpClusterApiBase.cs b/src/Clients/HttpClusterApiBase.cs new file mode 100644 index 0000000..107149e --- /dev/null +++ b/src/Clients/HttpClusterApiBase.cs @@ -0,0 +1,666 @@ +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Text.RegularExpressions; + +namespace BeetleX.FastHttpApi.Clients +{ + public class HttpClusterApi : IDisposable + { + public HttpClusterApi() + { + DefaultNode = new ApiNode("*"); + + DetectionTime = 2000; + mDetectionTimer = new System.Threading.Timer(OnVerifyClients, null, DetectionTime, DetectionTime); + + TimeOut = 30000; + } + + private long mVersion; + + + + private System.Collections.Concurrent.ConcurrentDictionary mHandlers = new System.Collections.Concurrent.ConcurrentDictionary(); + + private void ChangeVersion() + { + System.Threading.Interlocked.Increment(ref mVersion); + } + + public int TimeOut { get; set; } + + public long Version => mVersion; + + internal ClientActionHanler GetHandler(MethodInfo method) + { + ClientActionHanler result; + if (!mHandlers.TryGetValue(method, out result)) + { + result = new ClientActionHanler(method); + mHandlers[method] = result; + } + return result; + } + + private System.Threading.Timer mUploadNodeTimer; + + private ApiClusterInfo mLastClusterInfo = new ApiClusterInfo(); + + public int DetectionTime { get; set; } + + public INodeSourceHandler NodeSourceHandler { get; set; } + + public bool UpdateSuccess { get; set; } + + public Exception UpdateExption { get; set; } + + private void UpdateNodeInfo(ApiClusterInfo info) + { + + List removeUrls = new List(); + removeUrls.Add("*"); + foreach (string key in mNodes.Keys) + { + removeUrls.Add(key); + } + foreach (var item in info.Urls) + { + string url = item.Name.ToLower(); + if (item.Hosts.Count > 0) + { + removeUrls.Remove(url); + SetNode(url, item.GetNode()); + } + } + foreach (string item in removeUrls) + { + RemoveNode(item); + } + ChangeVersion(); + } + + private async void OnUploadNode_Callback(object sate) + { + mUploadNodeTimer.Change(-1, -1); + try + { + var info = await NodeSourceHandler.Load(); + if (info.Version != mLastClusterInfo.Version) + { + mLastClusterInfo = info; + UpdateNodeInfo(info); + } + UpdateSuccess = true; + UpdateExption = null; + } + catch (Exception e_) + { + UpdateSuccess = false; + UpdateExption = e_; + } + finally + { + mUploadNodeTimer.Change(NodeSourceHandler.UpdateTime * 1000, NodeSourceHandler.UpdateTime * 1000); + } + } + + public async Task LoadNodeSource(string cluster, params string[] hosts) + { + HTTPRemoteSourceHandler hTTPRemoteSourceHandler = new HTTPRemoteSourceHandler(cluster, hosts); + var result = await LoadNodeSource(hTTPRemoteSourceHandler); + return result; + + } + public async Task LoadNodeSource(INodeSourceHandler nodeSourceHandler) + { + NodeSourceHandler = nodeSourceHandler; + var info = await NodeSourceHandler.Load(); + UpdateNodeInfo(info); + mLastClusterInfo = info; + mUploadNodeTimer = new System.Threading.Timer(OnUploadNode_Callback, null, NodeSourceHandler.UpdateTime * 1000, NodeSourceHandler.UpdateTime * 1000); + return this; + } + + private System.Threading.Timer mDetectionTimer; + + + private ConcurrentDictionary mAgents = new ConcurrentDictionary(); + + private ConcurrentDictionary mNodes = new ConcurrentDictionary(); + + public ConcurrentDictionary Nodes => mNodes; + + public IApiNode DefaultNode { get; internal set; } + + private IApiNode OnGetNode(string url) + { + url = url.ToLower(); + if (mNodes.TryGetValue(url, out IApiNode node)) + return node; + return null; + } + + private void RemoveNode(string url) + { + ChangeVersion(); + if (url == "*") + DefaultNode = new ApiNode("*"); + else + mNodes.TryRemove(url, out IApiNode apiNode); + //SetNode(url, new ApiNode(url)); + if (mAgents.TryGetValue(url, out ApiNodeAgent agent)) + { + agent.Node = DefaultNode;//new ApiNode(url); + } + } + + private IApiNode MatchNode(string url) + { + foreach (string key in mNodes.Keys) + { + if (Regex.IsMatch(url, key, RegexOptions.IgnoreCase)) + { + return mNodes[key]; + } + } + return null; + } + + public ApiNodeAgent GetAgent(string url) + { + IApiNode node = MatchNode(url); + if (node == null) + node = DefaultNode; + if (!mAgents.TryGetValue(node.Url, out ApiNodeAgent agent)) + { + agent = new ApiNodeAgent(); + agent.Url = node.Url; + agent.Node = node; + mAgents[node.Url] = agent; + } + agent.Version = this.Version; + return agent; + } + + public HttpClusterApi SetNode(string url, IApiNode node) + { + if (url == "*") + DefaultNode = node; + else + mNodes[url.ToLower()] = node; + if (mAgents.TryGetValue(url, out ApiNodeAgent agent)) + { + agent.Node = node; + } + else + { + agent = new ApiNodeAgent(); + agent.Url = url; + agent.Node = node; + mAgents[url] = agent; + } + ChangeVersion(); + return this; + } + + private IApiNode CreateUrlNode(string url) + { + IApiNode node = new ApiNode(url); + mNodes[node.Url] = node; + ApiNodeAgent nodeAgent = new ApiNodeAgent(); + nodeAgent.Node = node; + nodeAgent.Url = url; + mAgents[node.Url] = nodeAgent; + ChangeVersion(); + return node; + } + + public IApiNode GetUrlNode(string url) + { + url = url.ToLower(); + if (url == "*") + return DefaultNode; + IApiNode node = OnGetNode(url); + if (node == null) + { + node = CreateUrlNode(url); + } + return node; + } + public HttpClusterApi AddHost(string url, string host, int weight = 10) + { + ChangeVersion(); + if (url == "*") + { + DefaultNode.Add(host, weight); + } + else + { + url = url.ToLower(); + IApiNode node = OnGetNode(url); + if (node == null) + { + node = CreateUrlNode(url); + } + node.Add(host, weight); + } + return this; + } + + public HttpClusterApi AddHost(string url, params string[] host) + { + ChangeVersion(); + if (url == "*") + { + if (host != null) + { + foreach (string item in host) + { + DefaultNode.Add(item); + } + } + } + else + { + if (host != null) + { + foreach (string item in host) + { + AddHost(url, item); + } + } + } + return this; + } + + private System.Collections.Concurrent.ConcurrentDictionary mAPI = new System.Collections.Concurrent.ConcurrentDictionary(); + + public T Create() + { + Type type = typeof(T); + object result; + if (!mAPI.TryGetValue(type, out result)) + { + result = DispatchProxy.Create(); + mAPI[type] = result; + ((HttpClusterApiProxy)result).Cluster = this; + } + return (T)result; + } + + private void OnVerifyClients(object state) + { + mDetectionTimer.Change(-1, -1); + try + { + foreach (IApiNode node in mNodes.Values) + { + node.Verify(); + } + + if (DefaultNode != null) + DefaultNode.Verify(); + } + catch { } + finally + { + mDetectionTimer.Change(DetectionTime, DetectionTime); + } + + } + + public ClusterStats Status() + { + ClusterStats result = new ClusterStats(); + foreach (var node in mNodes.Values) + { + result.Items.Add(new ClusterStats.UrlStats() { Url = node.Url, Node = node }); + } + result.Items.Add(new ClusterStats.UrlStats() { Url = "*", Node = DefaultNode }); + result.Items.Sort(); + return result; + } + + public void Dispose() + { + if (mUploadNodeTimer != null) + mUploadNodeTimer.Dispose(); + if (mDetectionTimer != null) + mDetectionTimer.Dispose(); + mHandlers.Clear(); + + } + } + + public class ClusterStats + { + + public ClusterStats() + { + Items = new List(); + } + public List Items { get; private set; } + + public class UrlStats : IComparable + { + + public string Url { get; set; } + + public IApiNode Node { get; set; } + + public int CompareTo(object obj) + { + return Url.CompareTo(((UrlStats)obj).Url); + } + } + + public override string ToString() + { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("HttpClusterApi"); + for (int i = 0; i < Items.Count; i++) + { + var item = Items[i]; + stringBuilder.AppendLine($" |-Url:{item.Url}"); + foreach (var client in item.Node.Hosts.ToArray()) + { + string available = client.Available ? "Y" : "N"; + stringBuilder.AppendLine($" |--{client.Host}[{available}][W:{client.Weight}] [{client}]"); + } + } + return stringBuilder.ToString(); + } + } + + public class HttpClusterApiProxy : System.Reflection.DispatchProxy + { + public HttpClusterApiProxy() + { + + } + + public HttpClusterApi Cluster { get; set; } + + protected override object Invoke(MethodInfo targetMethod, object[] args) + { + ClientActionHanler handler = Cluster.GetHandler((MethodInfo)targetMethod); + var rinfo = handler.GetRequestInfo(args); + if (handler.NodeAgent == null || handler.NodeAgent.Version != Cluster.Version) + handler.NodeAgent = Cluster.GetAgent(rinfo.Url); + HttpHost host = handler.NodeAgent.Node.GetClient(); + if (host == null) + { + Exception error = new HttpClientException(null, null, $"request {rinfo.Url} no http nodes are available"); + if (handler.Async) + { + if (handler.MethodType == typeof(ValueTask)) + return new ValueTask(Task.FromException(error)); + else + { + Type gtype = typeof(AnyCompletionSource<>); + Type type = gtype.MakeGenericType(handler.ReturnType); + IAnyCompletionSource source = (IAnyCompletionSource)Activator.CreateInstance(type); + source.Error(error); + return source.GetTask(); + } + } + else + { + throw error; + } + } + if (!handler.Async) + { + var request = rinfo.GetRequest(host); + var task = request.Execute(); + + if (!task.Wait(Cluster.TimeOut)) + { + throw new HttpClientException(request, host.Uri, $"{rinfo.Method} {rinfo.Url} request time out {Cluster.TimeOut}!"); + } + if (task.Result.Exception != null) + throw task.Result.Exception; + return task.Result.Body; + } + else + { + var request = rinfo.GetRequest(host); + var task = request.Execute(); + if (handler.MethodType == typeof(ValueTask)) + { + AnyCompletionSource source = new AnyCompletionSource(); + source.WaitResponse(task); + return new ValueTask(source.Task); + } + else + { + Type gtype = typeof(AnyCompletionSource<>); + Type type = gtype.MakeGenericType(handler.ReturnType); + IAnyCompletionSource source = (IAnyCompletionSource)Activator.CreateInstance(type); + source.WaitResponse(task); + return source.GetTask(); + } + } + + } + } + + public class ApiNodeAgent + { + public long Version { get; set; } + + public string Url { get; set; } + + public IApiNode Node { get; set; } + } + + public interface IApiNode + { + string Url { get; set; } + + IApiNode Add(string host, int weight); + + IApiNode Add(string host); + + IApiNode Add(IEnumerable hosts); + + HttpHost GetClient(); + + List Hosts { get; } + + void Verify(); + } + + public class ApiNode : IApiNode + { + public ApiNode(string url) + { + Url = url.ToLower(); + } + public string Url { get; set; } + + public List Hosts => mClients; + + private List mClients = new List(); + + private long mID = 1; + + public const int TABLE_SIZE = 50; + + private HttpHost[] mHttpHostTable; + + public bool Available { get; private set; } + + public long Status + { + get + { + long result = 0; + foreach (var item in mClients.ToArray()) + { + if (item.Available) + result |= item.ID; + else + result |= 0; + } + if (result > 0) + Available = true; + return result; + } + } + + private long mLastStatus = 0; + + internal void RefreshWeightTable() + { + var status = Status; + if (mLastStatus == status) + return; + else + mLastStatus = status; + HttpHost[] table = new HttpHost[TABLE_SIZE]; + int sum = 0; + mClients.Sort((x, y) => y.Weight.CompareTo(x.Weight)); + List aclients = new List(); + for (int i = 0; i < mClients.Count; i++) + { + if (mClients[i].Available) + { + sum += mClients[i].Weight; + aclients.Add(mClients[i]); + } + } + int count = 0; + for (int i = 0; i < aclients.Count; i++) + { + int size = (int)((double)aclients[i].Weight / (double)sum * (double)TABLE_SIZE); + for (int k = 0; k < size; k++) + { + table[count] = aclients[i]; + count++; + if (count >= TABLE_SIZE) + goto END; + } + } + int index = 0; + while (count < TABLE_SIZE) + { + table[count] = aclients[index % aclients.Count]; + index++; + count++; + } + END: + Shuffle(table); + mHttpHostTable = table; + } + + private void Shuffle(HttpHost[] list) + { + Random rng = new Random(); + int n = list.Length; + while (n > 1) + { + n--; + int k = rng.Next(n + 1); + HttpHost value = list[k]; + list[k] = list[n]; + list[n] = value; + } + } + + public IApiNode Add(IEnumerable hosts) + { + if (hosts != null) + { + foreach (var item in hosts) + { + Add(item, 10); + } + } + return this; + } + + public IApiNode Add(string host, int weight) + { + Uri url = new Uri(host); + if (mClients.Find(c => c.Host == url.Host && c.Port == url.Port) == null) + { + var item = new HttpHost(host); + item.ID = mID << mClients.Count; + item.Weight = weight; + item.MaxRPS = 0; + mClients.Add(item); + RefreshWeightTable(); + } + return this; + } + + public IApiNode Add(string host) + { + Add(host, 10); + return this; + } + + private long mIndex; + + public HttpHost GetClient() + { + if (Available) + { + HttpHost[] table = mHttpHostTable; + if (table == null || table.Length == 0) + return null; + int count = 0; + long index = System.Threading.Interlocked.Increment(ref mIndex); + while (count < TABLE_SIZE) + { + + HttpHost client = table[index % TABLE_SIZE]; + if (client.Available) + return client; + index++; + count++; + } + } + return null; + } + + public void Verify() + { + int count = 0; + for (int i = 0; i < mClients.Count; i++) + { + HttpHost client = mClients[i]; + if (!client.Available && !client.InVerify) + { + client.InVerify = true; + count++; + Task.Run(() => OnVerify(client)); + } + } + RefreshWeightTable(); + } + + private async void OnVerify(HttpHost host) + { + try + { + var request = host.Get("/", null, null, null, null); + var result = await request.Execute(); + } + catch + { + + } + finally + { + host.InVerify = false; + } + } + } + +} diff --git a/src/Clients/IBodyFormater.cs b/src/Clients/IBodyFormater.cs new file mode 100644 index 0000000..b00d2fe --- /dev/null +++ b/src/Clients/IBodyFormater.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; +using BeetleX.Buffers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BeetleX.FastHttpApi.Clients +{ + public interface IClientBodyFormater + { + string ContentType { get; } + + void Serialization(object data, PipeStream stream); + + object Deserialization(BeetleX.Buffers.PipeStream stream, Type type, int length); + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Class)] + public abstract class FormaterAttribute : Attribute, IClientBodyFormater + { + public abstract string ContentType { get; } + + public abstract void Serialization(object data, PipeStream stream); + + public abstract object Deserialization(BeetleX.Buffers.PipeStream stream, Type type, int length); + } + + public class FormUrlFormater : FormaterAttribute + { + public override string ContentType => "application/x-www-form-urlencoded"; + + public override object Deserialization(PipeStream stream, Type type, int length) + { + return stream.ReadString(length); + } + public override void Serialization(object data, PipeStream stream) + { + System.Collections.IDictionary keyValuePairs = data as IDictionary; + if (keyValuePairs != null) + { + int i = 0; + foreach (object key in keyValuePairs.Keys) + { + object value = keyValuePairs[key]; + if (value != null) + { + if (i > 0) + stream.Write("&"); + stream.Write(key.ToString() + "="); + if (value is string) + { + stream.Write(System.Net.WebUtility.UrlEncode((string)value)); + } + else + { + stream.Write(System.Net.WebUtility.UrlEncode(value.ToString())); + } + i++; + } + } + } + else + { + stream.Write(data.ToString()); + } + } + } + + public class JsonFormater : FormaterAttribute + { + public override string ContentType => "application/json"; + + public override object Deserialization(PipeStream stream, Type type, int length) + { + using (stream.LockFree()) + { + if (type == null) + { + using (System.IO.StreamReader streamReader = new System.IO.StreamReader(stream)) + using (JsonTextReader reader = new JsonTextReader(streamReader)) + { + JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(); + object token = jsonSerializer.Deserialize(reader); + return token; + } + } + else + { + using (StreamReader streamReader = new StreamReader(stream)) + { + JsonSerializer serializer = new JsonSerializer(); + object result = serializer.Deserialize(streamReader, type); + return result; + } + } + } + } + + public override void Serialization(object data, PipeStream stream) + { + using (stream.LockFree()) + { + using (StreamWriter writer = new StreamWriter(stream)) + { + IDictionary dictionary = data as IDictionary; + JsonSerializer serializer = new JsonSerializer(); + if (dictionary != null && dictionary.Count == 1) + { + object[] vlaues = new object[dictionary.Count]; + dictionary.Values.CopyTo(vlaues, 0); + serializer.Serialize(writer, vlaues[0]); + } + else + { + serializer.Serialize(writer, data); + } + } + } + } + } +} diff --git a/src/Clients/INodeSourcesHandler.cs b/src/Clients/INodeSourcesHandler.cs new file mode 100644 index 0000000..c10829a --- /dev/null +++ b/src/Clients/INodeSourcesHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace BeetleX.FastHttpApi.Clients +{ + public class NodeSource + { + public string Url { get; set; } + + public IApiNode Node { get; set; } + } + + public interface INodeSourceHandler + { + int UpdateTime { get; set; } + Task Load(); + } + + [JsonFormater] + public interface IHttpSourceApi + { + [Get] + Task _GetCluster(string cluster = "default"); + } + + public class HTTPRemoteSourceHandler : INodeSourceHandler + { + private HttpClusterApi mHttpClusterApi = new HttpClusterApi(); + + private IHttpSourceApi mRemoteSourceApi; + + public HTTPRemoteSourceHandler(string name, params string[] hosts) + { + mHttpClusterApi.AddHost("*", hosts); + mRemoteSourceApi = mHttpClusterApi.Create(); + Name = name; + } + + public string Name { get; set; } + + public int UpdateTime { get; set; } = 5; + + public Task Load() + { + return mRemoteSourceApi._GetCluster(Name); + } + } + + + public class ApiClusterInfo + { + public ApiClusterInfo() + { + Urls = new List(); + } + + public string Name { get; set; } + + public string Version { get; set; } + + public List Urls { get; set; } + } + + public class UrlNodeInfo + { + + public UrlNodeInfo() + { + Hosts = new List(); + } + + public string Name { get; set; } + + public List Hosts { get; set; } + + public ApiNode GetNode() + { + ApiNode result = new ApiNode(Name); + foreach (var item in Hosts) + { + result.Add(item.Name, item.Weight); + } + return result; + + } + } + + public class UrlHostInfo + { + public string Name { get; set; } + + public int Weight { get; set; } + + public int MaxRPS { get; set; } + } +} diff --git a/src/Clients/Request.cs b/src/Clients/Request.cs new file mode 100644 index 0000000..15c7581 --- /dev/null +++ b/src/Clients/Request.cs @@ -0,0 +1,220 @@ +using BeetleX.Buffers; +using BeetleX.Clients; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace BeetleX.FastHttpApi.Clients +{ + public class Request + { + public const string POST = "POST"; + + public const string GET = "GET"; + + public const string DELETE = "DELETE"; + + public const string PUT = "PUT"; + + public Request() + { + Method = GET; + this.HttpProtocol = "HTTP/1.1"; + } + + public IClientBodyFormater Formater { get; set; } + + public Dictionary QuestryString { get; set; } + + public Header Header { get; set; } + + public string Url { get; set; } + + public string Method { get; set; } + + public string HttpProtocol { get; set; } + + public Response Response { get; set; } + + public Object Body { get; set; } + + public Type BodyType { get; set; } + + public RequestStatus Status { get; set; } + + public IClient Client { get; set; } + + internal void Execute(PipeStream stream) + { + var buffer = HttpParse.GetByteBuffer(); + int offset = 0; + offset += Encoding.ASCII.GetBytes(Method, 0, Method.Length, buffer, offset); + buffer[offset] = HeaderTypeFactory._SPACE_BYTE; + offset++; + offset += Encoding.ASCII.GetBytes(Url, 0, Url.Length, buffer, offset); + if (QuestryString != null && QuestryString.Count > 0) + { + int i = 0; + foreach (var item in this.QuestryString) + { + string key = item.Key; + string value = item.Value; + if (string.IsNullOrEmpty(value)) + continue; + value = System.Net.WebUtility.UrlEncode(value); + if (i == 0) + { + buffer[offset] = HeaderTypeFactory._QMARK; + offset++; + } + else + { + buffer[offset] = HeaderTypeFactory._AND; + offset++; + } + offset += Encoding.ASCII.GetBytes(key, 0, key.Length, buffer, offset); + buffer[offset] = HeaderTypeFactory._EQ; + offset++; + offset += Encoding.ASCII.GetBytes(value, 0, value.Length, buffer, offset); + i++; + } + } + buffer[offset] = HeaderTypeFactory._SPACE_BYTE; + offset++; + offset += Encoding.ASCII.GetBytes(HttpProtocol, 0, HttpProtocol.Length, buffer, offset); + buffer[offset] = HeaderTypeFactory._LINE_R; + offset++; + buffer[offset] = HeaderTypeFactory._LINE_N; + offset++; + stream.Write(buffer, 0, offset); + if (Header != null) + Header.Write(stream); + if (Method == POST || Method == PUT) + { + if (Body != null) + { + stream.Write(HeaderTypeFactory.CONTENT_LENGTH_BYTES, 0, 16); + MemoryBlockCollection contentLength = stream.Allocate(10); + stream.Write(HeaderTypeFactory.TOW_LINE_BYTES, 0, 4); + int len = stream.CacheLength; + Formater.Serialization(Body, stream); + int count = stream.CacheLength - len; + contentLength.Full(count.ToString().PadRight(10), stream.Encoding); + } + else + { + stream.Write(HeaderTypeFactory.NULL_CONTENT_LENGTH_BYTES, 0, HeaderTypeFactory.NULL_CONTENT_LENGTH_BYTES.Length); + stream.Write(HeaderTypeFactory.LINE_BYTES, 0, 2); + } + } + else + { + stream.Write(HeaderTypeFactory.LINE_BYTES, 0, 2); + } + } + + public HttpHost HttpHost { get; set; } + + public Task Execute() + { + AnyCompletionSource taskCompletionSource = new AnyCompletionSource(); + OnExecute(taskCompletionSource); + return taskCompletionSource.Task; + } + + private async void OnExecute(AnyCompletionSource taskCompletionSource) + { + HttpClient client = null; + Response response; + try + { + client = HttpHost.Pool.Pop(); + client.RequestCommpletionSource = taskCompletionSource; + Client = client.Client; + if (client.Client is AsyncTcpClient) + { + AsyncTcpClient asyncClient = (AsyncTcpClient)client.Client; + var a = asyncClient.ReceiveMessage(); + if (!a.IsCompleted) + { + asyncClient.Send(this); + Status = RequestStatus.SendCompleted; + } + var result = await a; + if (result is Exception error) + { + response = new Response(); + response.Exception = new HttpClientException(this, HttpHost.Uri, error.Message, error); + Status = RequestStatus.Error; + } + else + { + response = (Response)result; + Status = RequestStatus.Received; + } + } + else + { + TcpClient syncClient = (TcpClient)client.Client; + syncClient.SendMessage(this); + Status = RequestStatus.SendCompleted; + response = syncClient.ReceiveMessage(); + Status = RequestStatus.Received; + } + if (response.Exception == null) + { + int code = int.Parse(response.Code); + if (response.Length > 0) + { + try + { + if (code < 400) + response.Body = this.Formater.Deserialization(response.Stream, this.BodyType, response.Length); + else + response.Body = response.Stream.ReadString(response.Length); + } + finally + { + response.Stream.ReadFree(response.Length); + if (response.Chunked) + response.Stream.Dispose(); + response.Stream = null; + } + } + if (!response.KeepAlive) + client.Client.DisConnect(); + if (code >= 400) + response.Exception = new HttpClientException(this, HttpHost.Uri, $" [{this.Method} {this.Url} {response.Code} {response.CodeMsg}] Response text:[{response.Body}]"); + Status = RequestStatus.Completed; + } + } + catch (Exception e_) + { + HttpClientException clientException = new HttpClientException(this, HttpHost.Uri, e_.Message, e_); + response = new Response { Exception = clientException }; + Status = RequestStatus.Error; + } + if (response.Exception != null) + HttpHost.AddError(response.Exception.SocketError); + else + HttpHost.AddSuccess(); + Response.Current = response; + this.Response = response; + if (client != null) + { + HttpHost.Pool.Push(client); + } + await Task.Run(() => taskCompletionSource.TrySetResult(response)); + } + } + + public enum RequestStatus + { + None, + SendCompleted, + Received, + Completed, + Error + } +} diff --git a/src/Clients/Response.cs b/src/Clients/Response.cs new file mode 100644 index 0000000..95f39a4 --- /dev/null +++ b/src/Clients/Response.cs @@ -0,0 +1,102 @@ +using BeetleX.Buffers; +using System; +using System.Collections.Generic; + +using System.Text; + +namespace BeetleX.FastHttpApi.Clients +{ + public class Response + { + + public Response() + { + Header = new Header(); + Cookies = new Cookies(); + mState = LoadedState.None; + this.KeepAlive = true; + } + + public HttpClientException Exception { get; set; } + + public Cookies Cookies { get; private set; } + + public string Code { get; set; } + + public string CodeMsg { get; set; } + + public bool KeepAlive { get; set; } + + public string HttpVersion { get; set; } + + public object Body { get; set; } + + public int Length { get; set; } + + public Header Header { get; private set; } + + public bool Chunked { get; set; } + + public bool Gzip { get; set; } + + internal PipeStream Stream { get; set; } + + private LoadedState mState; + + public LoadedState Load(PipeStream stream) + { + string line; + if (mState == LoadedState.None) + { + if (stream.TryReadWith(HeaderTypeFactory.LINE_BYTES, out line)) + { + HttpParse.AnalyzeResponseLine(line, this); + mState = LoadedState.Method; + } + } + if (mState == LoadedState.Method) + { + if (Header.Read(stream, Cookies)) + { + mState = LoadedState.Header; + } + } + if (mState == LoadedState.Header) + { + if (string.Compare(Header[HeaderTypeFactory.CONNECTION], "close", true) == 0) + { + this.KeepAlive = false; + } + if (string.Compare(Header[HeaderTypeFactory.TRANSFER_ENCODING], "chunked", true) == 0) + { + Chunked = true; + } + else + { + string lenstr = Header[HeaderTypeFactory.CONTENT_LENGTH]; + int length = 0; + if (lenstr != null) + int.TryParse(lenstr, out length); + Length = length; + } + mState = LoadedState.Completed; + } + return mState; + } + + + [ThreadStatic] + private static Response mCurrent; + public static Response Current + { + get + { + return mCurrent; + } + set + { + mCurrent = value; + } + } + } +} diff --git a/src/CommandLineArgs.cs b/src/CommandLineArgs.cs new file mode 100644 index 0000000..996f980 --- /dev/null +++ b/src/CommandLineArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + class CommandLineArgs + { + [Option("-host", Required = false)] + public string Host { get; set; } + [Option("-port", Required = false)] + public int Port { get; set; } + [Option("-sslport", Required = false)] + public int SSLPort { get; set; } + + [Option("-sslfile", Required = false)] + public string SSLFile { get; set; } + + [Option("-sslpwd", Required = false)] + public string SSLPassWord { get; set; } + + [Option("-sock", Required = false)] + public string Sock { get; set; } + } +} diff --git a/src/ConfigBase.cs b/src/ConfigBase.cs new file mode 100644 index 0000000..ec6d860 --- /dev/null +++ b/src/ConfigBase.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + public class ConfigBase where T : new() + { + + public void Save() + { + string filename = typeof(T).Name + ".json"; + string config = Newtonsoft.Json.JsonConvert.SerializeObject(this); + lock (this) + { + using (System.IO.StreamWriter writer = new System.IO.StreamWriter(filename, false, Encoding.UTF8)) + { + writer.Write(config); + writer.Flush(); + } + } + } + + private static T mInstance = default(T); + + public static T Instance + { + get + { + if (mInstance == null) + { + string filename = typeof(T).Name + ".json"; + if (System.IO.File.Exists(filename)) + { + using (System.IO.StreamReader reader = new System.IO.StreamReader(filename, Encoding.UTF8)) + { + string value = reader.ReadToEnd(); + mInstance = Newtonsoft.Json.JsonConvert.DeserializeObject(value); + } + } + else + { + mInstance = new T(); + } + } + return mInstance; + } + } + + public T GetInstance() + { + return Instance; + } + + } +} diff --git a/src/ControllerAttribute.cs b/src/ControllerAttribute.cs index 3834bb6..50cd420 100644 --- a/src/ControllerAttribute.cs +++ b/src/ControllerAttribute.cs @@ -15,6 +15,8 @@ public ControllerAttribute() public InstanceType InstanceType { get; set; } + public string ActorTag { get; set; } + public bool SkipPublicFilter { get; set; } = false; } diff --git a/src/DatBuffer.cs b/src/DatBuffer.cs new file mode 100644 index 0000000..c32299a --- /dev/null +++ b/src/DatBuffer.cs @@ -0,0 +1,27 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + public class DataBuffer : IDisposable + { + public DataBuffer(int length) + { + Length = length; + Data = ArrayPool.Shared.Rent(length); + } + + public T[] Data { get; set; } + + public int Length { get; set; } + + public int Offset { get; set; } + + public void Dispose() + { + ArrayPool.Shared.Return(Data); + } + } +} diff --git a/src/Data/DataContext.cs b/src/Data/DataContext.cs index 4909335..5149981 100644 --- a/src/Data/DataContext.cs +++ b/src/Data/DataContext.cs @@ -61,7 +61,7 @@ public bool TryGetBoolean(string name, out bool value) public bool TryGetDateTime(string name, out DateTime value) { object data; - value = DateTime.Now; + value = DateTime.MinValue; if (GetProperty(name, out data)) { if (data is JProperty token) diff --git a/src/EventArgs.cs b/src/EventArgs.cs index 6a61507..6d0736d 100644 --- a/src/EventArgs.cs +++ b/src/EventArgs.cs @@ -8,6 +8,28 @@ namespace BeetleX.FastHttpApi { public delegate void EventHttpServerLog(IServer server, BeetleX.EventArgs.ServerLogEventArgs e); + public class ActionExecutedArgs + { + public string ServerType { get; set; } + + public string Url { get; set; } + + public string HTTPMethod { get; set; } + + public ActionHandler ActionHandler { get; set; } + + public IDictionary Headers { get; set; } + + public long UseTime { get; set; } + + public DateTime Time { get; set; } + + public int Code { get; set; } = 200; + + public Exception Exception { get; set; } + + } + public class HttpServerLogEventArgs : BeetleX.EventArgs.ServerLogEventArgs { public HttpServerLogEventArgs(object tag, string message, LogType type, ISession session = null) @@ -50,13 +72,20 @@ public double Time public class WebSocketConnectArgs : System.EventArgs { - public WebSocketConnectArgs(HttpRequest request) + public WebSocketConnectArgs(HttpRequest request, HttpResponse response) { Request = request; + Response = response; Cancel = false; } + public UpgradeWebsocketError Error { get; set; } + + public IResult UpgradeSuccess { get; set; } + + public HttpResponse Response { get; internal set; } + public HttpRequest Request { get; internal set; } public bool Cancel { get; set; } @@ -105,6 +134,16 @@ public class EventHttpRequestArgs : System.EventArgs public bool Cancel { get; set; } } + public class EventHttpInnerErrorArgs : EventHttpRequestArgs + { + public string Code { get; internal set; } + + public string Message { get; internal set; } + + public Exception Error { get; internal set; } + + } + public class EventHttpServerStartedArgs : System.EventArgs { public HttpApiServer HttpApiServer { get; internal set; } diff --git a/src/FileLog.cs b/src/FileLog.cs index a5077ed..64ea1a1 100644 --- a/src/FileLog.cs +++ b/src/FileLog.cs @@ -59,11 +59,12 @@ private void OnWriteLog(LogItem e) { mWriteCount++; System.IO.StreamWriter writer = GetWriter(); - writer.Write("["); writer.Write(DateTime.Now); - writer.Write("] ["); + writer.Write("\t"); writer.Write(e.Type.ToString()); - writer.Write("] "); + writer.Write("\t"); + writer.Write(e.RemoveIP!=null? e.RemoveIP:"SYSTEM"); + writer.Write("\t"); writer.WriteLine(e.Message); if (mWriteCount > 200 || mDispatcher.Count == 0) { @@ -72,9 +73,9 @@ private void OnWriteLog(LogItem e) } } - public void Add(LogType type, string message) + public void Add(string removeIP,LogType type, string message) { - Add(new LogItem(type, message)); + Add(new LogItem(removeIP, type, message)); } public void Add(LogItem e) @@ -84,11 +85,13 @@ public void Add(LogItem e) public class LogItem { - public LogItem(LogType type, string message) + public LogItem(string removeIP, LogType type, string message) { + RemoveIP = removeIP; Type = type; Message = message; } + public string RemoveIP; public LogType Type; public string Message; } @@ -106,5 +109,6 @@ public class LogRecord public string Time { get; set; } public string Message { get; set; } + public string RemoveIP { get; set; } } } diff --git a/src/Header.cs b/src/Header.cs index 4baff15..81c2b69 100644 --- a/src/Header.cs +++ b/src/Header.cs @@ -101,6 +101,8 @@ public class HeaderTypeFactory public const string CLIENT_IPADDRESS = "X-Real-IP"; + public const string CLIENT_ENDPOINT = "X-Real-ENDPOINT"; + public const string SEC_WEBSOCKET_VERSION = "Sec_WebSocket_Version"; public const string SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; @@ -361,7 +363,7 @@ public bool Read(PipeStream stream, Cookies cookies) ReadOnlySpan line = lineData; Tuple result = HttpParse.AnalyzeHeader(line); this[result.Item1] = result.Item2; - if (line[0] == 'C' && line[5] == 'e' && line[1] == 'o' && line[2] == 'o' && line[3] == 'k' && line[4] == 'i') + if ((line[0] == 'C' || line[0] == 'c') && line[5] == 'e' && line[1] == 'o' && line[2] == 'o' && line[3] == 'k' && line[4] == 'i') { HttpParse.AnalyzeCookie(line.Slice(8, line.Length - 8), cookies); } diff --git a/src/HttpApiServer.cs b/src/HttpApiServer.cs index 5c37976..26b5a89 100644 --- a/src/HttpApiServer.cs +++ b/src/HttpApiServer.cs @@ -14,16 +14,25 @@ using System.Reflection; using System.Collections.Concurrent; using System.Runtime; +using System.Security.Cryptography; +using System.Threading.Tasks; +using System.Buffers; +using System.Security.Principal; +using Newtonsoft.Json; namespace BeetleX.FastHttpApi { - public class HttpApiServer : ServerHandlerBase, BeetleX.ISessionSocketProcessHandler, WebSockets.IDataFrameSerializer, IWebSocketServer, IDisposable + public partial class HttpApiServer : ServerHandlerBase, BeetleX.ISessionSocketProcessHandler, WebSockets.IDataFrameSerializer, IWebSocketServer, IDisposable { public const int WEBSOCKET_SUCCESS = 250; public const int WEBSOCKET_ERROR = 550; + public const string LICENSE_URL = "/__GET_SN"; + + public const string CODE_TREAK_PARENTID = "parent-id"; + public HttpApiServer() : this(null) { @@ -45,8 +54,12 @@ public HttpApiServer(HttpOptions options) mIPv4Tables.Load(); mActionFactory = new ActionHandlerFactory(this); mResourceCenter = new StaticResurce.ResourceCenter(this); + mResourceCenter.Debug = Options.Debug; mUrlRewrite = new RouteRewrite(this); mModuleManager = new ModuleManager(this); + AllRpsLimit = new RpsLimit(Options.MaxRps); + this.LoadLicenses(); + CommandLineParser = CommandLineParser.GetCommandLineParser(); } const string mConfigFile = "HttpConfig.json"; @@ -63,6 +76,8 @@ public HttpApiServer(HttpOptions options) private ServerCounter mServerCounter; + public CommandLineParser CommandLineParser { get; private set; } + public RouteRewrite UrlRewrite => mUrlRewrite; public ServerCounter ServerCounter => mServerCounter; @@ -90,10 +105,14 @@ public LogRecord[] GetCacheLog() private int mCacheLogLength = 0; + public Dictionary ActionExts { get; private set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + private ConcurrentDictionary mProperties = new ConcurrentDictionary(); public long RequestErrors => mRequestErrors; + public IPLimit IPLimit => mIPLimit; + public IPv4Tables IPv4Tables => mIPv4Tables; public ModuleManager ModuleManager => mModuleManager; @@ -110,8 +129,119 @@ public LogRecord[] GetCacheLog() public IDataFrameSerializer FrameSerializer { get; set; } + private Dictionary> mLicenses = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private ObjectPoolGroup mRequestPool = new ObjectPoolGroup(); + private string[] mBindDomains = new string[0]; + + private static Dictionary LoadLicenseInfo(string info) + { + + try + { + Dictionary data = Newtonsoft.Json.JsonConvert.DeserializeObject>(info); + using (RSA rsa = RSA.Create(2048)) + { + RSAParameters rSAParameters = Newtonsoft.Json.JsonConvert.DeserializeObject( + "{\"D\": null, \"DP\": null, \"DQ\": null, \"Exponent\": \"AQAB\", \"InverseQ\": null, \"Modulus\": \"00yOCUKe6cixa0Hr1upIRZVYXuncVYCVXzNtl9PAyjyEfxny4QtWVUuw3MK/DF1vx51QBtS3izb1dhQYdwmoD2FGZy7TehnZ9AvVEuesWvJtz6Npm6zmHM1wVYqYrCkyWzIgKenyv59yHgYgL55UN8c3oqqwg2voeq+12IscYjYAXpqWywA7x+33TsTz4J1wQzMaq5VM+6yPZuqDa7sCyubK6qlUpHy790Iy7bD0y48pGrlZOHibcA6NOCAu/LsqKJWCX/38tx8HEzEE+i8TyuhLhMaQpFGTUBgP5iJ9ONviu/irvIXlP68atFdntmBs2cvWktxNJzmcGB24upjatQ==\", \"P\": null, \"Q\": null}"); + rsa.ImportParameters(rSAParameters); + + string values = ""; + string token = ""; + foreach (var item + in from a in data orderby a.Key descending select a) + { + if (item.Key == "token") + { + token = item.Value; + } + else + { + values += item.Value; + } + } + + if (rsa.VerifyData(Encoding.UTF8.GetBytes(values), Convert.FromBase64String(token), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)) + { + data.Remove("token"); + return data; + } + return new Dictionary(); + } + + } + catch (Exception e_) + { + return new Dictionary(); + } + } + + private void LoadLicenses() + { + foreach (var file in System.IO.Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.sn")) + { + try + { + using (StreamReader reader = new StreamReader(file, Encoding.UTF8)) + { + string txt = reader.ReadToEnd(); + var lincense = LoadLicenseInfo(txt); + if (lincense != null) + { + if (lincense.TryGetValue("InvalidDate", out string date)) + { + if (DateTime.Now > DateTime.Parse(date)) + continue; + } + string name = System.IO.Path.GetFileNameWithoutExtension(file); + mLicenses[name] = lincense; + } + } + } + catch (Exception e) + { + + } + } + } + + public void SetBindDomains(string domains) + { + Options.BindDomains = domains; + if (!string.IsNullOrEmpty(Options.BindDomains)) + { + mBindDomains = Options.BindDomains.Split(';'); + + } + else + { + mBindDomains = new string[0]; + } + + } + + private bool CheckDomains(HttpRequest request) + { + if (mBindDomains == null || mBindDomains.Length == 0) + return true; + var host = request.Host; + if (string.IsNullOrEmpty(host)) + return false; + foreach (var item in mBindDomains) + { + if (host.IndexOf(item) >= 0) + return true; + } + return false; + } + + public Dictionary GetLicense(string name) + { + mLicenses.TryGetValue(name, out Dictionary result); + return result; + } + internal HttpRequest CreateRequest(ISession session) { HttpToken token = (HttpToken)session.Tag; @@ -129,6 +259,13 @@ internal void Recovery(HttpRequest request) } } + public RpsLimit AllRpsLimit { get; private set; } + + + public UrlLimit UrlsLimit { get; private set; } + + public UrlLimit DomainsLimit { get; private set; } + public void SaveOptions() { string file = Directory.GetCurrentDirectory() + System.IO.Path.DirectorySeparatorChar + mConfigFile; @@ -187,6 +324,10 @@ public object this[string name] public long TotalConnections => mTotalConnections; + public Func WSActionResultHandler { get; set; } + + public System.Collections.Concurrent.ConcurrentDictionary RpsLimitHandlers { get; private set; } = new ConcurrentDictionary(); + public EventHandler WebSocketReceive { get; set; } public event EventHandler HttpConnecting; @@ -199,17 +340,21 @@ public object this[string name] public event EventHandler HttpRequestNotfound; + public event EventHandler HttpInnerError; + public event EventHandler HttpDisconnect; public event EventHandler Started; public event EventHandler ActionExecuting; + public event EventHandler ActionExecuted; + public event EventHandler OptionLoad; public event EventHandler HttpResponsed; - public EventHandler WebSocketConnect { get; set; } + public event EventHandler WebSocketConnect; private List mAssemblies = new List(); @@ -221,7 +366,6 @@ public object this[string name] public void Register(params System.Reflection.Assembly[] assemblies) { - //mUrlRewrite.UrlIgnoreCase = Options.UrlIgnoreCase; mAssemblies.AddRange(assemblies); try { @@ -229,7 +373,26 @@ public void Register(params System.Reflection.Assembly[] assemblies) } catch (Exception e_) { - Log(LogType.Error, "http api server load controller error " + e_.Message + "@" + e_.StackTrace); + Log(LogType.Error, null, "http api server load controller error " + e_.Message + "@" + e_.StackTrace); + } + } + + private void AutoLoad() + { + foreach (var item in System.IO.Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")) + { + try + { + var assembly = Assembly.LoadFile(item); + if (assembly.GetCustomAttribute() != null) + { + Register(assembly); + } + } + catch (Exception e) + { + + } } } @@ -249,51 +412,74 @@ public virtual void IncrementResponsed(HttpRequest request, HttpResponse respons catch (Exception e_) { if (EnableLog(LogType.Error)) - Log(LogType.Error, $"{request.Session.RemoteEndPoint} {request.Method} {request.Url} responsed event error {e_.Message}@{e_.StackTrace}"); + Log(LogType.Error, request.Session, $"{request.Session.RemoteEndPoint} {request.Method} {request.Url} responsed event error {e_.Message}@{e_.StackTrace}"); } } - private void InitFromCommandLineArgs(IServer server) + + + public async Task Open() { - string[] lines = System.Environment.GetCommandLineArgs(); - if (lines != null) - { - foreach (var item in lines) - { - var values = item.Split('='); - if (values.Length == 2) - { - if (string.Compare(values[0], "host", true) == 0) - { - server.Options.DefaultListen.Host = values[1]; - } - if (string.Compare(values[0], "port", true) == 0) - { - if (int.TryParse(values[1], out int port)) - { - server.Options.DefaultListen.Port = port -; - } - } - } - } - } + + var result = Task.Run(() => + { + try + { + OnOpen(); + } + catch (Exception e_) + { + Log(LogType.Error, null, $"HTTP open error {e_.Message}@{e_.StackTrace}!"); + } + }); + await result; } - public void Open() + + private void OnOpen() { + AutoLoad(); var date = GMTDate.Default.DATE; var ct = ContentTypes.TEXT_UTF8; var a = HeaderTypeFactory.Find("Content-Length"); AppDomain.CurrentDomain.AssemblyResolve += ResolveHandler; + if (Options.WebSocketFrameSerializer != null) + this.FrameSerializer = Options.WebSocketFrameSerializer; HttpPacket hp = new HttpPacket(this, this.FrameSerializer); var gtmdate = GMTDate.Default; - string serverInfo = $"Server: beetlex.io\r\n"; + string serverInfo = $"Server: {Options.ServerTag}\r\n"; HeaderTypeFactory.SERVAR_HEADER_BYTES = Encoding.ASCII.GetBytes(serverInfo); + + CommandLineArgs commandLineArgs = this.CommandLineParser.GetOption(); + if (!string.IsNullOrEmpty(commandLineArgs.SSLFile)) + { + + Options.SSL = true; + Options.CertificateFile = commandLineArgs.SSLFile; + Options.CertificatePassword = commandLineArgs.SSLPassWord; + } + if (!string.IsNullOrEmpty(commandLineArgs.Host)) + { + Options.Host = commandLineArgs.Host; + } + if (commandLineArgs.Port > 0) + { + Options.Port = commandLineArgs.Port; + } + if (commandLineArgs.SSLPort > 0) + { + Options.SSLPort = commandLineArgs.SSLPort; + } + if (!string.IsNullOrEmpty(commandLineArgs.Sock)) + { + Options.SockFile = commandLineArgs.Sock; + } mServer = SocketFactory.CreateTcpServer(this, hp) .Setting(o => { o.SyncAccept = Options.SyncAccept; // o.IOQueues = Options.IOQueues; + if (Options.SSLOnly) + o.DefaultListen.Enabled = false; o.DefaultListen.Host = Options.Host; // o.IOQueueEnabled = Options.IOQueueEnabled; o.DefaultListen.Port = Options.Port; @@ -310,6 +496,7 @@ public void Open() o.PrivateBufferPoolSize = Options.MaxBodyLength; o.MaxWaitMessages = Options.MaxWaitQueue; }); + SetBindDomains(Options.BindDomains); if (Options.IOQueueEnabled) { mRequestIOQueues = new DispatchCenter(OnIOQueueProcess, Options.IOQueues); @@ -318,7 +505,15 @@ public void Open() { mServer.Setting(o => { - o.AddListenSSL(Options.CertificateFile, Options.CertificatePassword, o.DefaultListen.Host, Options.SSLPort); + o.AddListenSSL(Options.CertificateFile, Options.CertificatePassword, Options.SslProtocols, o.DefaultListen.Host, Options.SSLPort); + }); + } + UnixSocketUri unixsocket = SocketFactory.GetUnixSocketUrl(Options.SockFile); + if (unixsocket.IsUnixSocket) + { + mServer.Setting(o => + { + o.AddListen(unixsocket.SockFile, 0); }); } Name = "BeetleX Http Server"; @@ -326,7 +521,17 @@ public void Open() { foreach (System.Reflection.Assembly assembly in mAssemblies) { - mResourceCenter.LoadManifestResource(assembly); + try + { + if (assembly.GetCustomAttribute() == null) + { + mResourceCenter.LoadManifestResource(assembly); + } + } + catch (Exception e_) + { + Log(LogType.Warring, null, $"HTTP resource center load {assembly.FullName} assembly error {e_.Message}"); + } } } mResourceCenter.LoadManifestResource(typeof(HttpApiServer).Assembly); @@ -341,12 +546,16 @@ public void Open() } StartTime = DateTime.Now; mServer.WriteLogo = WriteLogo ?? OutputLogo; - InitFromCommandLineArgs(mServer); + + mIPLimit = new IPLimit(this); + mIPLimit.Load(); + UrlsLimit = new UrlLimit(); + UrlsLimit.Load(); + DomainsLimit = new UrlLimit(); + DomainsLimit.Load(); mServer.Open(); mServerCounter = new ServerCounter(this); - // mUrlRewrite.UrlIgnoreCase = Options.UrlIgnoreCase; mUrlRewrite.Load(); - //mUrlRewrite.AddRegion(this.Options.Routes); HeaderTypeFactory.Find(HeaderTypeFactory.HOST); AppDomain.CurrentDomain.UnhandledException += (s, e) => { @@ -372,7 +581,7 @@ public void Open() writer.Flush(); } }; - mIPLimit = new IPLimit(this); + OnOptionLoad(new EventOptionsReloadArgs { HttpApiServer = this, HttpOptions = this.Options }); OnStrated(new EventHttpServerStartedArgs { HttpApiServer = this }); if (Options.Virtuals != null) @@ -381,12 +590,25 @@ public void Open() { item.Verify(); if (EnableLog(LogType.Info)) - Log(LogType.Info, $"Set virtual folder {item.Folder} to {item.Path}"); + Log(LogType.Info, null, $"Set virtual folder {item.Folder} to {item.Path}"); } } + this.LoadActionExts(); } + private void LoadActionExts() + { + if (!string.IsNullOrEmpty(Options.ActionExt)) + { + foreach (var item in Options.ActionExt.Split(';')) + { + ActionExts[item] = item; + } + AddExts(Options.ActionExt); + } + } + public void AddExts(string exts) { exts = exts.ToLower(); @@ -444,11 +666,12 @@ http and websocket framework logo += " -----------------------------------------------------------------------------\r\n"; foreach (var item in mServer.Options.Listens) { - logo += $" {item}\r\n"; + if (item.Enabled) + logo += $" {item}\r\n"; } logo += " -----------------------------------------------------------------------------\r\n"; - Log(LogType.Info, logo); + Log(LogType.Info, null, logo); } @@ -464,12 +687,12 @@ public void SaveActionSettings() { mActionSettings.Save(ActionFactory.Handlers); if (EnableLog(LogType.Info)) - Log(LogType.Info, $"HTTP save actions settings success"); + Log(LogType.Info, null, $"HTTP save actions settings success"); } catch (Exception e_) { if (EnableLog(LogType.Error)) - Log(LogType.Error, $"HTTP save actions settings error {e_.Message}@{e_.StackTrace}"); + Log(LogType.Error, null, $"HTTP save actions settings error {e_.Message}@{e_.StackTrace}"); } } @@ -487,7 +710,7 @@ public Assembly ResolveHandler(object sender, ResolveEventArgs args) catch (Exception e_) { if (EnableLog(LogType.Warring)) - Log(LogType.Warring, $"{args.RequestingAssembly.FullName} load assembly {args.Name} error {e_.Message}"); + Log(LogType.Warring, null, $"{args.RequestingAssembly.FullName} load assembly {args.Name} error {e_.Message}"); } return null; } @@ -495,6 +718,7 @@ public Assembly ResolveHandler(object sender, ResolveEventArgs args) public string Name { get { return mServer.Name; } set { mServer.Name = value; } } + public void SendToWebSocket(DataFrame data, Func filter = null) { IList items = GetWebSockets(); @@ -542,6 +766,8 @@ public override void Connected(IServer server, ConnectedEventArgs e) System.Threading.Interlocked.Increment(ref mTotalConnections); HttpToken token = new HttpToken(); token.Request = new HttpRequest(); + token.HttpRpsLimit = new RpsLimit(Options.SessionMaxRps); + token.WSRpsLimit = new RpsLimit(Options.WebSocketSessionMaxRps); token.Request.Init(e.Session, this); e.Session.Tag = token; e.Session.SocketProcessHandler = this; @@ -561,6 +787,7 @@ public override void Disconnect(IServer server, SessionEventArgs e) if (token.Request != null) token.Request.Response = null; token.Request = null; + token.WebSocketData?.Dispose(); } if (LogOutput == e.Session) @@ -585,18 +812,18 @@ public override void Disconnect(IServer server, SessionEventArgs e) // Log(type, tag, string.Format(message, parameters)); //} - public void Log(LogType type, object tag, string message) + public void Log(LogType type, ISession session, object tag, string message) { try { - Log(null, new HttpServerLogEventArgs(tag, message, type)); + Log(null, new HttpServerLogEventArgs(tag, message, type, session)); } catch { } } - public void Log(LogType type, string message) + public void Log(LogType type, ISession session, string message) { - Log(type, null, message); + Log(type, session, null, message); } public override void Connecting(IServer server, ConnectingEventArgs e) @@ -604,32 +831,17 @@ public override void Connecting(IServer server, ConnectingEventArgs e) if (server.Count > Options.MaxConnections) { e.Cancel = true; - if (EnableLog(LogType.Warring)) - { - Log(LogType.Warring, $"HTTP ${e.Socket.RemoteEndPoint} out of max connections!"); - } - } - IPEndPoint ipPoint = (IPEndPoint)e.Socket.RemoteEndPoint; - if (!IPv4Tables.Verify(ipPoint.Address)) - { - e.Cancel = true; - if (EnableLog(LogType.Warring)) + if (EnableLog(LogType.Info)) { - Log(LogType.Warring, $"HTTP ${e.Socket.RemoteEndPoint} IPv4 tables verify no permission!"); + Log(LogType.Info, null, $"HTTP ${e.Socket.RemoteEndPoint} out of max connections!"); } } - if (Options.IPRpsLimit > 0) + if (e.Socket.RemoteEndPoint is IPEndPoint ipPoint) { - string ip = ((IPEndPoint)e.Socket.RemoteEndPoint).Address.ToString(); - var item = mIPLimit.GetItem(ip); - if (item != null) - { - if (!item.Enabled) - e.Cancel = true; - } + e.Socket.NoDelay = true; } HttpConnecting?.Invoke(this, e); - e.Socket.NoDelay = true; + } public override void Error(IServer server, ServerErrorEventArgs e) @@ -639,14 +851,18 @@ public override void Error(IServer server, ServerErrorEventArgs e) #region websocket - public virtual object FrameDeserialize(DataFrame data, PipeStream stream) + public virtual object FrameDeserialize(DataFrame data, PipeStream stream, HttpRequest request) { - return stream.ReadString((int)data.Length); + // + DataBuffer buffer = new DataBuffer((int)data.Length); + stream.Read(buffer.Data, 0, buffer.Length); + return buffer; + //return stream.ReadString((int)data.Length); } private System.Collections.Concurrent.ConcurrentQueue mBuffers = new System.Collections.Concurrent.ConcurrentQueue(); - public virtual ArraySegment FrameSerialize(DataFrame data, object body) + public virtual ArraySegment FrameSerialize(DataFrame data, object body, HttpRequest request) { byte[] result; if (!mBuffers.TryDequeue(out result)) @@ -678,6 +894,8 @@ private void OnWebSocketConnect(HttpRequest request, HttpResponse response) HttpToken token = (HttpToken)request.Session.Tag; token.KeepAlive = true; token.WebSocket = true; + token.WebSocketData = new PipeStream(this.Server.ReceiveBufferPool.Next()); + token.WebSocketJsonSerializer = new JsonSerializer(); request.WebSocket = true; if (EnableLog(LogType.Info)) { @@ -685,12 +903,11 @@ private void OnWebSocketConnect(HttpRequest request, HttpResponse response) } ConnectionUpgradeWebsocket(request, response); - } protected virtual void ConnectionUpgradeWebsocket(HttpRequest request, HttpResponse response) { - WebSocketConnectArgs wsca = new WebSocketConnectArgs(request); + WebSocketConnectArgs wsca = new WebSocketConnectArgs(request, response); wsca.Request = request; WebSocketConnect?.Invoke(this, wsca); if (wsca.Cancel) @@ -699,22 +916,51 @@ protected virtual void ConnectionUpgradeWebsocket(HttpRequest request, HttpRespo { mServer.Log(LogType.Warring, request.Session, $"HTTP {request.ID} {request.Session.RemoteEndPoint} cancel upgrade to websocket"); } - response.Session.Dispose(); + if (wsca.Error == null) + wsca.Error = new UpgradeWebsocketError(500, "websocket upgrade cancel"); + response.Result(wsca.Error); + Close(request.Session); + } else { - UpgradeWebsocketResult upgradeWebsocket = new UpgradeWebsocketResult(request.Header[HeaderTypeFactory.SEC_WEBSOCKET_KEY]); - response.Result(upgradeWebsocket); + if (wsca.UpgradeSuccess != null) + { + response.Result(wsca.UpgradeSuccess); + } + else + { + UpgradeWebsocketSuccess upgradeWebsocket = new UpgradeWebsocketSuccess(request.Header[HeaderTypeFactory.SEC_WEBSOCKET_KEY]); + response.Result(upgradeWebsocket); + } } } - public void ExecuteWS(HttpRequest request, DataFrame dataFrame) + public virtual void ExecuteWS(HttpRequest request, HttpToken httpToken) { - JToken dataToken = (JToken)Newtonsoft.Json.JsonConvert.DeserializeObject((string)dataFrame.Body); - this.mActionFactory.ExecuteWithWS(request, this, dataToken); + using (httpToken.WebSocketData.LockFree()) + { + using (JsonTextReader reader = new JsonTextReader(new StreamReader(httpToken.WebSocketData))) + { + JToken dataToken = (JToken)httpToken.WebSocketJsonSerializer.Deserialize(reader); + this.mActionFactory.ExecuteWS(request, this, dataToken); + } + } } + public DataFrame CreateTextFrame(object body) + { + var result = CreateDataFrame(body); + result.Type = DataPacketType.text; + return result; + } + public DataFrame CreateBinaryFrame(object body) + { + var result = CreateDataFrame(body); + result.Type = DataPacketType.binary; + return result; + } public DataFrame CreateDataFrame(object body = null) { DataFrame dp = new DataFrame(this); @@ -725,14 +971,11 @@ public DataFrame CreateDataFrame(object body = null) protected virtual void OnWebSocketRequest(HttpRequest request, ISession session, DataFrame data) { - - if (Options.WebSocketMaxRPS > 0 && session.Count > Options.WebSocketMaxRPS) + HttpToken token = (HttpToken)session.Tag; + if (token.WSRpsLimit.Check(this.Options.SessionMaxRps)) { - if (EnableLog(LogType.Error)) - { - mServer.Log(LogType.Error, session, $"Websocket {request.ID} {request.RemoteIPAddress} session message queuing exceeds maximum rps!"); - } - session.Dispose(); + var frame = CreateTextFrame("session max rps limit!"); + session.Send(frame); return; } @@ -740,7 +983,7 @@ protected virtual void OnWebSocketRequest(HttpRequest request, ISession session, { mServer.Log(LogType.Info, session, $"Websocket {request.ID} {request.RemoteIPAddress} receive {data.Type.ToString()}"); } - HttpToken token = (HttpToken)session.Tag; + if (data.Type == DataPacketType.ping) { DataFrame pong = CreateDataFrame(); @@ -754,17 +997,50 @@ protected virtual void OnWebSocketRequest(HttpRequest request, ISession session, } else { - if (WebSocketReceive == null) + if (AllRpsLimit.Check(this.Options.MaxRps)) { - if (data.Type == DataPacketType.text) - { - ExecuteWS(token.Request, data); - } + var frame = CreateTextFrame("server max rps limit!"); + session.Send(frame); + return; } - else - { - try + DataBuffer databuffer = data.Body as DataBuffer; + try + { + if (data.Type == DataPacketType.text || data.Type == DataPacketType.binary) + { + token.WSLastPacketType = data.Type; + } + if (WebSocketReceive == null) + { + if (data.Type == DataPacketType.text || (data.Type == DataPacketType.continuation && token.WSLastPacketType == DataPacketType.text)) + { + token.WebSocketData?.Write(databuffer.Data, 0, databuffer.Length); + if (data.FIN) + { + token.WebSocketData.Position = 0; + try + { + ExecuteWS(token.Request, token); + } + finally + { + if (token.WebSocketData.Length > 0) + token.WebSocketData.ReadFree((int)token.WebSocketData.Length); + } + } + } + else + { + var args = new WebSocketReceiveArgs(); + args.Frame = data; + args.Sesson = session; + args.Server = this; + args.Request = token.Request; + WebSocketReceive?.Invoke(this, args); + } + } + else { var args = new WebSocketReceiveArgs(); args.Frame = data; @@ -773,10 +1049,11 @@ protected virtual void OnWebSocketRequest(HttpRequest request, ISession session, args.Request = token.Request; WebSocketReceive?.Invoke(this, args); } - finally - { - } + } + finally + { + databuffer?.Dispose(); } } @@ -789,8 +1066,22 @@ private void CacheLog(ServerLogEventArgs e) { if (Options.CacheLogMaxSize > 0) { + HttpToken token = e.Session != null ? (HttpToken)e.Session.Tag : null; + string removip = token?.Request?.RemoteEndPoint; + if (removip == null) + removip = "SYSTEM"; + if (Options.CacheLogFilter != null) + { + if (removip.IndexOf(Options.CacheLogFilter) == -1) + { + return; + } + } + LogRecord record = new LogRecord(); record.Type = e.Type.ToString(); + record.RemoveIP = token?.Request?.RemoteEndPoint; + record.RemoveIP = removip; record.Message = e.Message; record.Time = DateTime.Now.ToString("H:mm:ss"); System.Threading.Interlocked.Increment(ref mCacheLogLength); @@ -806,12 +1097,21 @@ private void CacheLog(ServerLogEventArgs e) public override void Log(IServer server, ServerLogEventArgs e) { var httpLog = e as HttpServerLogEventArgs; + HttpToken token = e.Session != null ? (HttpToken)e.Session.Tag : null; CacheLog(e); ServerLog?.Invoke(server, e); if (Options.LogToConsole && (httpLog == null || httpLog.OutputConsole)) base.Log(server, e); if (Options.WriteLog && (httpLog == null || httpLog.OutputFile)) - mFileLog.Add(e.Type, e.Message); + { + var endPoint = token?.Request?.RemoteEndPoint; + if (endPoint == null) + endPoint = e.Session?.RemoteEndPoint.ToString(); + var localPoint = token?.Request?.Session?.Socket?.LocalEndPoint.ToString(); + if (localPoint == null) + localPoint = e.Session?.Socket.LocalEndPoint.ToString(); + mFileLog.Add(endPoint + "/" + localPoint, e.Type, e.Message); + } ISession output = LogOutput; if (output != null && e.Session != output) { @@ -821,17 +1121,66 @@ public override void Log(IServer server, ServerLogEventArgs e) } } + private object mLockConsole = new object(); + + protected override void OnLogToConsole(IServer server, ServerLogEventArgs e) + { + lock (mLockConsole) + { + HttpToken token = e.Session != null ? (HttpToken)e.Session.Tag : null; + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write($">>{ DateTime.Now.ToString("HH:mmm:ss")}"); + switch (e.Type) + { + case LogType.Error: + Console.ForegroundColor = ConsoleColor.DarkRed; + break; + case LogType.Warring: + Console.ForegroundColor = ConsoleColor.Yellow; + break; + case LogType.Fatal: + Console.ForegroundColor = ConsoleColor.Red; + break; + case LogType.Info: + Console.ForegroundColor = ConsoleColor.Green; + break; + default: + Console.ForegroundColor = ConsoleColor.White; + break; + } + Console.Write($" [{e.Type.ToString().PadRight(7)}]"); + var endPoint = token?.Request?.RemoteEndPoint; + if (endPoint == null) + endPoint = e.Session?.RemoteEndPoint.ToString(); + var localPoint = token?.Request?.Session?.Socket?.LocalEndPoint.ToString(); + if (localPoint == null) + localPoint = e.Session?.Socket.LocalEndPoint.ToString(); + if (endPoint == null) + { + Console.ForegroundColor = ConsoleColor.DarkCyan; + Console.Write($" [SYSTEM] "); + } + else + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write($" {endPoint}/{localPoint} "); + } + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine(e.Message); + } + } + protected virtual void OnOptionLoad(EventOptionsReloadArgs e) { if (EnableLog(LogType.Debug)) - Log(LogType.Debug, "HTTP server options reload event!"); + Log(LogType.Debug, null, "HTTP server options reload event!"); OptionLoad?.Invoke(this, e); } protected virtual void OnStrated(EventHttpServerStartedArgs e) { if (EnableLog(LogType.Debug)) - Log(LogType.Debug, "HTTP server started event!"); + Log(LogType.Debug, null, "HTTP server started event!"); Started?.Invoke(this, e); } @@ -852,17 +1201,18 @@ protected virtual void OnProcessResource(HttpRequest request, HttpResponse respo { BaseServer.Error(e_, request.Session, $"{request.RemoteIPAddress} {request.Method} {request.BaseUrl} file error {e_.Message}"); } - InnerErrorResult result = new InnerErrorResult($"response file error ", e_, Options.OutputStackTrace); - response.Result(result); + //InnerErrorResult result = new InnerErrorResult($"response file error ", e_, Options.OutputStackTrace); + //response.Result(result); + response.InnerError($"response file error!", e_, Options.OutputStackTrace); } } else { if (EnableLog(LogType.Info)) - Log(LogType.Info, $"{request.RemoteIPAddress}{request.Method} {request.Url} not support"); - NotSupportResult notSupport = new NotSupportResult($"{request.Method} {request.Url} not support"); - response.Result(notSupport); - + Log(LogType.Info, request.Session, $"{request.RemoteIPAddress}{request.Method} {request.Url} not support"); + //NotSupportResult notSupport = new NotSupportResult($"{request.Method} {request.Url} not support"); + //response.Result(notSupport); + response.InnerError("403", $"{request.Method} method not support!"); } } finally @@ -895,11 +1245,15 @@ private void OnIOQueueProcess(IOQueueProcessArgs e) } } + private void OnRequestHandler(PacketDecodeCompletedEventArgs e) { try { + if (e.Session.IsDisposed) + return; HttpToken token = (HttpToken)e.Session.Tag; + if (token.WebSocket) { OnWebSocketRequest(token.Request, e.Session, (WebSockets.DataFrame)e.Message); @@ -910,7 +1264,7 @@ private void OnRequestHandler(PacketDecodeCompletedEventArgs e) if (EnableLog(LogType.Info)) { - mServer.Log(LogType.Info, null, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {request.Url}"); + mServer.Log(LogType.Info, e.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {request.Url}"); } if (EnableLog(LogType.Debug)) { @@ -919,13 +1273,7 @@ private void OnRequestHandler(PacketDecodeCompletedEventArgs e) request.Server = this; HttpResponse response = request.CreateResponse(); token.KeepAlive = request.KeepAlive; - if (!mIPLimit.ValidateRPS(request)) - { - token.KeepAlive = false; - InnerErrorResult innerErrorResult = new InnerErrorResult("400", $"{request.RemoteIPAddress} request limit!"); - response.Result(innerErrorResult); - return; - } + if (token.FirstRequest && string.Compare(request.Header[HeaderTypeFactory.UPGRADE], "websocket", true) == 0) { token.FirstRequest = false; @@ -973,7 +1321,7 @@ internal void OnActionRegisting(EventActionRegistingArgs e) ActionRegisting?.Invoke(this, e); } - internal bool OnActionExecuting(IHttpContext context,ActionHandler handler) + internal bool OnActionExecuting(IHttpContext context, ActionHandler handler) { if (ActionExecuting != null) { @@ -1016,12 +1364,126 @@ public ServerCounter.ServerStatus GetServerInfo() return new ServerCounter.ServerStatus(); } + private void OnOutputLicense(HttpRequest request, HttpResponse response) + { + var name = request.Data["name"]; + var info = GetLicense(name); + if (info == null) + info = new Dictionary(); + JsonResult result = new JsonResult(info); + response.Result(result); + } + private bool CheckUrlLimit(HttpRequest request, HttpResponse response) + { + var url = request.BaseUrl; + if (string.IsNullOrEmpty(url)) + return true; + if (UrlsLimit.Count == 0) + return true; + var limit = UrlsLimit.Match(url, request); + if (!limit.ValidateRPS()) + { + response.InnerError("509", $"{url} max rps limit!"); + return false; + } + return true; + } + private bool CheckDomainsLimit(HttpRequest request, HttpResponse response) + { + var url = request.Host; + if (string.IsNullOrEmpty(url)) + return true; + if (DomainsLimit.Count == 0) + return true; + var limit = DomainsLimit.Match(url, request); + if (!limit.ValidateRPS()) + { + response.InnerError("509", $"{url} max rps limit!"); + return false; + } + return true; + } + private bool CheckIPTable(HttpRequest request, HttpResponse response) + { + if (IPv4Tables.Type == IPv4Tables.VerifyType.None) + return true; + if (!IPv4Tables.Verify(request.RemoteIPAddress)) + { + var msg = $"HTTP ${request.RemoteIPAddress} IP tables verify no permission!"; + response.InnerError("509", msg); + return false; + } + return true; + + } protected virtual void OnHttpRequest(HttpRequest request, HttpResponse response) { + string baseUrl = request.BaseUrl; + if (baseUrl.Length == LICENSE_URL.Length) + { + if (baseUrl[1] == '_' && baseUrl[2] == '_') + { + if (string.Compare(baseUrl, LICENSE_URL, true) == 0) + { + OnOutputLicense(request, response); + return; + } + } + } + if (!CheckIPTable(request, response)) + return; + if (!CheckDomains(request)) + { + if (string.IsNullOrEmpty(Options.InvalidDomainUrl)) + { + response.InnerError("509", "Invalid domain name!"); + } + else + { + Move302Result result = new Move302Result(Options.InvalidDomainUrl); + response.Result(result); + } + return; + } + HttpToken token = (HttpToken)request.Session.Tag; + if (token.HttpRpsLimit.Check(this.Options.SessionMaxRps)) + { + response.InnerError("509", "session max rps limit!"); + return; + } + if (!mIPLimit.ValidateRPS(request)) + { + response.InnerError("509", $"{request.RemoteIPAddress} max rps limit!"); + return; + } + if (!CheckUrlLimit(request, response)) + return; + if (!CheckDomainsLimit(request, response)) + return; + if (RpsLimitHandlers.Count > 0) + foreach (var handler in RpsLimitHandlers.Values) + { + if (handler.Check(request, response)) + { + response.InnerError("509", $"{handler.Name} max rps limit!"); + return; + } + } + + if (AllRpsLimit.Check(this.Options.MaxRps)) + { + response.InnerError("509", "server max rps limit!"); + return; + } + + if (!OnHttpRequesting(request, response).Cancel) { - string baseUrl = request.BaseUrl; - if (string.IsNullOrEmpty(request.Ext) && baseUrl[baseUrl.Length - 1] != '/') + if (OnExecuteMap(request)) + { + return; + } + if ((string.IsNullOrEmpty(request.Ext) || ActionExts.ContainsKey(request.Ext)) && baseUrl[baseUrl.Length - 1] != '/') { mActionFactory.Execute(request, response, this); } @@ -1032,29 +1494,56 @@ protected virtual void OnHttpRequest(HttpRequest request, HttpResponse response) } } + internal void OnInnerError(HttpResponse response, string code, string message, Exception e, bool outputStackTrace) + { + if (HttpInnerError != null) + { + EventHttpInnerErrorArgs error = new EventHttpInnerErrorArgs + { + Code = code, + Message = message, + Error = e, + Request = response.Request, + Response = response + }; + HttpInnerError.Invoke(this, error); + if (error.Cancel) + return; + } + var result = new InnerErrorResult(code, message, e, outputStackTrace); + response.Result(result); + + } public virtual void ReceiveCompleted(ISession session, SocketAsyncEventArgs e) { } + public override void SessionDetection(IServer server, SessionDetectionEventArgs e) + { + base.SessionDetection(server, e); + } - public virtual void SendCompleted(ISession session, SocketAsyncEventArgs e) + public virtual void SendCompleted(ISession session, SocketAsyncEventArgs e, bool end) { - HttpToken token = (HttpToken)session.Tag; - if (token.File != null) + if (end) { - token.File = token.File.Next(); + HttpToken token = (HttpToken)session.Tag; if (token.File != null) { - session.Send(token.File); - return; + token.File = token.File.Next(); + if (token.File != null) + { + session.Send(token.File); + return; + } + } + if (session.Count == 0 && !token.KeepAlive) + { + session.Dispose(); } - } - if (session.Count == 0 && !token.KeepAlive) - { - session.Dispose(); } } @@ -1101,6 +1590,18 @@ public bool EnableLog(LogType logType) return (int)(this.Options.LogLevel) <= (int)logType; } + public async void Close(ISession session, int delay = 1000) + { + try + { + await Task.Delay(delay); + session?.Dispose(); + } + catch (Exception e_) + { + + } + } public HttpApiServer GetLog(LogType logType) { if (EnableLog(logType)) @@ -1115,5 +1616,50 @@ public void Dispose() if (BaseServer != null) BaseServer.Dispose(); } + + internal void OnActionExecutedError(IHttpContext context, ActionHandler handler, Exception error, int code, long startTime) + { + if (ActionExecuted != null) + { + ActionExecutedArgs e = new ActionExecutedArgs(); + if (context.WebSocket) + { + e.ServerType = "Websocket"; + } + else + { + e.ServerType = "HTTP"; + e.HTTPMethod = context.Request.Method; + } + e.Code = code; + e.Exception = error; + e.Headers = context.Request.Header.Copy(); + e.ActionHandler = handler; + e.Url = context.Request.Url; + e.UseTime = context.Server.BaseServer.GetRunTime() - startTime; + ActionExecuted(this, e); + } + } + internal void OnActionExecutedSuccess(IHttpContext context, ActionHandler handler, long startTime) + { + if (ActionExecuted != null) + { + ActionExecutedArgs e = new ActionExecutedArgs(); + if (context.WebSocket) + { + e.ServerType = "Websocket"; + } + else + { + e.ServerType = "HTTP"; + e.HTTPMethod = context.Request.Method; + } + e.Headers = context.Request.Header.Copy(); + e.ActionHandler = handler; + e.Url = context.Request.Url; + e.UseTime = context.Server.BaseServer.GetRunTime() - startTime; + ActionExecuted(this, e); + } + } } } diff --git a/src/HttpApiServer_map.cs b/src/HttpApiServer_map.cs new file mode 100644 index 0000000..4f9bc0c --- /dev/null +++ b/src/HttpApiServer_map.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace BeetleX.FastHttpApi +{ + public partial class HttpApiServer + { + private Dictionary> mMapUrlAction = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + private Dictionary>> mMapUrlTaskAction = new Dictionary>>(StringComparer.OrdinalIgnoreCase); + + private string OnGetMapURL(string route) + { + var ra = new RouteTemplateAttribute(route); + if (route[0] != '/') + route = '/' + route; + var reurl = ra.Analysis(null); + string url = route; + if (!string.IsNullOrEmpty(reurl)) + { + url = route.Substring(0, route.IndexOf("{")); + if (url[url.Length - 1] == '/') + { + url = url.Substring(0, url.Length - 1); + } + UrlRewrite.Add(null, reurl, url); + } + return url; + } + + private bool OnExecuteMap(HttpRequest request) + { + if (mMapUrlAction.Count == 0 && mMapUrlTaskAction.Count == 0) + return false; + HttpContext context = new HttpContext(this, request, request.Response, request.Data); + if (mMapUrlAction.TryGetValue(request.BaseUrl, out Func hanlder)) + { + OnExecuteMapAction(hanlder, context); + return true; + } + if (mMapUrlTaskAction.TryGetValue(request.BaseUrl, out Func> action)) + { + OnExecuteMapFun(action, context); + return true; + } + return false; + } + + private void OnExecuteMapAction(Func action, IHttpContext context) + { + try + { + var result = action(context); + context.Result(result); + } + catch (Exception e_) + { + //context.Result(new InnerErrorResult("500", e_, this.Options.OutputStackTrace)); + context.Response.InnerError("500","execute map action error!", e_, this.Options.OutputStackTrace); + GetLog(EventArgs.LogType.Error)?.Log(EventArgs.LogType.Error, context.Session, $"HTTP Map {context.Request.BaseUrl} execute error {e_.Message} {e_.StackTrace}"); + } + } + + private async void OnExecuteMapFun(Func> action, IHttpContext context) + { + try + { + var result = await action(context); + context.Result(result); + } + catch (Exception e_) + { + //context.Result(new InnerErrorResult("500", e_, this.Options.OutputStackTrace)); + context.Response.InnerError("500", "execute map action error!", e_, this.Options.OutputStackTrace); + GetLog(EventArgs.LogType.Error)?.Log(EventArgs.LogType.Error, context.Session, $"HTTP Map {context.Request.BaseUrl} execute error {e_.Message} {e_.StackTrace}"); + } + } + + public HttpApiServer Map(string url, Func action) + { + var mapurl = OnGetMapURL(url); + mMapUrlAction[mapurl] = action; + return this; + } + + public HttpApiServer Map(string url, Func> action) + { + var mapurl = OnGetMapURL(url); + mMapUrlTaskAction[mapurl] = action; + return this; + } + } +} diff --git a/src/HttpOptions.cs b/src/HttpOptions.cs index 92218d0..12213b1 100644 --- a/src/HttpOptions.cs +++ b/src/HttpOptions.cs @@ -1,10 +1,12 @@ using BeetleX.EventArgs; using BeetleX.FastHttpApi; +using BeetleX.FastHttpApi.WebSockets; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Security.Authentication; using System.Text; namespace BeetleX.FastHttpApi @@ -29,7 +31,7 @@ public HttpOptions() NoGzipFiles = "jpg;jpeg;png;gif;png;ico;zip;rar"; CacheFiles = "html;htm;js;css"; BufferSize = 1024 * 4; - WebSocketMaxRPS = 30; + WebSocketSessionMaxRps = 0; LogLevel = EventArgs.LogType.Warring; LogToConsole = false; NotLoadFolder = @"\Files;\Images"; @@ -52,7 +54,13 @@ public HttpOptions() threads = 1; IOQueues = Math.Min(threads, 16); BufferPoolGroups = 4; + + } + [JsonIgnore] + public JsonSerializerSettings JsonSerializerSettings { get; set; } = new JsonSerializerSettings(); + + public string SockFile { get; set; } [Conditional("DEBUG")] public void SetDebug(string viewpath = null) @@ -70,9 +78,34 @@ public void SetDebug(string viewpath = null) } } + //当IP被限制请求后,是否禁止IP连接 + public bool DisableIPAccept { get; set; } = false; + + public string BindDomains { get; set; } + + public string InvalidDomainUrl { get; set; } + + public bool DisableXRealIP { get; set; } = false; + + public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls11 | SslProtocols.Tls12; + + public string ActionExt { get; set; } + + public int MaxRps { get; set; } = 0; + + public int SessionMaxRps { get; set; } = 0; + + public SameSiteType? SameSite { get; set; } + + public bool CookieSecure { get; set; } + + public string ServerTag { get; set; } = "beetlex.io"; + + public bool OutputServerTag { get; set; } = true; + public int IPRpsLimit { get; set; } = 0; - public int IPRpsLimitDisableTime { get; set; } = 1000 * 1800; + public int IPRpsLimitDisableTime { get; set; } =0; public int MaxWaitQueue { get; set; } = 50; @@ -82,7 +115,7 @@ public void SetDebug(string viewpath = null) public int IOQueues { get; set; } - public bool SyncAccept { get; set; } = true; + public bool SyncAccept { get; set; } = false; public bool ManageApiEnabled { get; set; } = true; @@ -92,6 +125,10 @@ public void SetDebug(string viewpath = null) public int CacheLogMaxSize { get; set; } + public string CacheLogFilter { get; set; } + + public IDataFrameSerializer WebSocketFrameSerializer { get; set; } + public List MaxrpsSettings { get; set; } public List Settings { get; set; } @@ -126,6 +163,8 @@ public void SetDebug(string viewpath = null) public bool LogToConsole { get; set; } + + public string NotLoadFolder { get; set; } public string CacheFiles { get; set; } @@ -134,7 +173,7 @@ public void SetDebug(string viewpath = null) public LogType LogLevel { get; set; } - public int WebSocketMaxRPS { get; set; } + public int WebSocketSessionMaxRps { get; set; } public int BufferSize { get; set; } @@ -162,6 +201,8 @@ public void SetDebug(string viewpath = null) public int Port { get; set; } + public bool SSLOnly { get; set; } = false; + public bool SSL { get; set; } public int SSLPort { get; set; } diff --git a/src/HttpPacket.cs b/src/HttpPacket.cs index bf597a1..cc96e7c 100644 --- a/src/HttpPacket.cs +++ b/src/HttpPacket.cs @@ -20,6 +20,8 @@ public HttpPacket(HttpApiServer server, IDataFrameSerializer dataPacketSerialize public EventHandler Completed { get; set; } + public string Name => "HTTP-SERVER"; + public IPacket Clone() { return new HttpPacket(mServer, this.mDataPacketSerializer); @@ -79,8 +81,9 @@ private void OnHttpDecode(ISession session, PipeStream pstream) } token.KeepAlive = false; var response = mRequest.CreateResponse(); - InnerErrorResult innerErrorResult = new InnerErrorResult("400", "Request http receive data error!"); - response.Result(innerErrorResult); + //InnerErrorResult innerErrorResult = new InnerErrorResult("400", "Request http receive data error!"); + //response.Result(innerErrorResult); + response.InnerError("400", "request http receive data error!"); return; } if (pstream.FirstBuffer.Length > 10) @@ -98,8 +101,9 @@ private void OnHttpDecode(ISession session, PipeStream pstream) } token.KeepAlive = false; var response = mRequest.CreateResponse(); - InnerErrorResult innerErrorResult = new InnerErrorResult("400", "Request http protocol data error!"); - response.Result(innerErrorResult); + //InnerErrorResult innerErrorResult = new InnerErrorResult("400", "Request http protocol data error!"); + //response.Result(innerErrorResult); + response.InnerError("400", "request http protocol data error!"); return; } } @@ -112,8 +116,9 @@ private void OnHttpDecode(ISession session, PipeStream pstream) } token.KeepAlive = false; var response = mRequest.CreateResponse(); - InnerErrorResult innerErrorResult = new InnerErrorResult("400", "Request header too large"); - response.Result(innerErrorResult); + //InnerErrorResult innerErrorResult = new InnerErrorResult("400", "Request header too large"); + //response.Result(innerErrorResult); + response.InnerError("400", "request header too large"); } else if (mRequest.Length > mServerConfig.MaxBodyLength) { @@ -123,11 +128,12 @@ private void OnHttpDecode(ISession session, PipeStream pstream) } token.KeepAlive = false; var response = mRequest.CreateResponse(); - InnerErrorResult innerErrorResult = new InnerErrorResult("400", "Request entity too large"); - response.Result(innerErrorResult); + //InnerErrorResult innerErrorResult = new InnerErrorResult("400", "Request entity too large"); + //response.Result(innerErrorResult); + response.InnerError("400", "request entity too large"); return; } - if (mReceives > 10 & pstream.Length < mReceives * 256) + if (mReceives > 10 && pstream.Length < mReceives * 256) { if (session.Server.EnableLog(LogType.Warring)) { @@ -135,34 +141,36 @@ private void OnHttpDecode(ISession session, PipeStream pstream) } token.KeepAlive = false; var response = mRequest.CreateResponse(); - InnerErrorResult innerErrorResult = new InnerErrorResult("400", "protocol data commit exception"); - response.Result(innerErrorResult); + //InnerErrorResult innerErrorResult = new InnerErrorResult("400", "protocol data commit exception"); + //response.Result(innerErrorResult); + response.InnerError("400", "protocol data commit exception"); } return; } } private void OnWebSocketDecode(ISession session, PipeStream pstream) { - START: + START: if (mDataPacket == null) { mDataPacket = new DataFrame(mServer); mDataPacket.DataPacketSerializer = this.mDataPacketSerializer; } mReceives++; + HttpToken token = (HttpToken)session.Tag; + mDataPacket.Request = token.Request; if (mDataPacket.Read(pstream) == DataPacketLoadStep.Completed) { mWebSocketRequest++; long now = session.Server.GetRunTime(); if (now - mLastTime < 1000) { - if (mServerConfig.WebSocketMaxRPS > 0 && mWebSocketRequest > mServerConfig.WebSocketMaxRPS) + if (mServerConfig.WebSocketSessionMaxRps > 0 && mWebSocketRequest > mServerConfig.WebSocketSessionMaxRps) { if (session.Server.EnableLog(LogType.Warring)) { session.Server.Log(LogType.Warring, session, $"Websocket {mRequest?.ID} {session.RemoteEndPoint} session rps limit!"); } - HttpToken token = (HttpToken)session.Tag; token.KeepAlive = false; var error = new ActionResult(500, "session rps limit!"); var frame = mServer.CreateDataFrame(error); @@ -261,6 +269,9 @@ private void OnEncode(ISession session, object data, System.IO.Stream stream) { PipeStream pstream = stream.ToPipeStream(); IDataResponse dataResponse = data as IDataResponse; + HttpToken token = (HttpToken)session.Tag; + if (data is DataFrame df) + df.Request = token.Request; if (dataResponse != null) { dataResponse.Write(pstream); diff --git a/src/HttpRequest.cs b/src/HttpRequest.cs index 7ede1cc..a50bf83 100644 --- a/src/HttpRequest.cs +++ b/src/HttpRequest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Http.Headers; using System.Text; namespace BeetleX.FastHttpApi @@ -90,7 +91,7 @@ internal HttpResponse CreateResponse() if (Response == null) { Response = new HttpResponse(); - Response.JsonSerializer = new Newtonsoft.Json.JsonSerializer(); + Response.JsonSerializer = Newtonsoft.Json.JsonSerializer.Create(Server.Options.JsonSerializerSettings); Response.StreamWriter = new StreamWriter(Session.Stream.ToPipeStream()); Response.JsonWriter = new Newtonsoft.Json.JsonTextWriter(Response.StreamWriter); } @@ -169,16 +170,56 @@ public void Recovery() public int Length => mLength; + public string TrackParentID + { + get + { + string value = Header[HttpApiServer.CODE_TREAK_PARENTID]; + return value; + } + } + + public string RemoteEndPoint + { + get + { + string value = Header[HeaderTypeFactory.CLIENT_ENDPOINT]; + if (value == null) + { + if (Session.RemoteEndPoint is IPEndPoint IP) + { + value = IP.ToString(); + Header[HeaderTypeFactory.CLIENT_ENDPOINT] = value; + } + } + return value; + } + } + public string RemoteIPAddress { get { - string value = Header[HeaderTypeFactory.CLIENT_IPADDRESS]; + string value = null; + if (Server.Options.DisableXRealIP) + { + value = Header[HeaderTypeFactory.CLIENT_IPADDRESS]; + if (value == null) + { + if (Session.RemoteEndPoint is IPEndPoint IP) + { + value= IP.Address.MapToIPv4().ToString(); + Header[HeaderTypeFactory.CLIENT_IPADDRESS] = value; + } + } + return value; + } + value = Header[HeaderTypeFactory.CLIENT_IPADDRESS]; if (value == null) { if (Session.RemoteEndPoint is IPEndPoint IP) { - value = IP.Address.ToString(); + value = IP.Address.MapToIPv4().ToString(); Header[HeaderTypeFactory.CLIENT_IPADDRESS] = value; } } @@ -363,7 +404,14 @@ public void UrlRewriteTo(string url) HttpParse.ReadUrlPathAndExt(Url.AsSpan(), mQueryString, this, this.Server.Options); if (Server.EnableLog(EventArgs.LogType.Info)) { - Server.BaseServer.Log(EventArgs.LogType.Info, Session, $"HTTP {ID} {((IPEndPoint)Session.RemoteEndPoint).Address} request {SourceUrl} rewrite to {Url}"); + if (Session.RemoteEndPoint is IPEndPoint ipPoint) + { + Server.BaseServer.Log(EventArgs.LogType.Info, Session, $"HTTP {ID} {ipPoint.Address} request {SourceUrl} rewrite to {Url}"); + } + else + { + Server.BaseServer.Log(EventArgs.LogType.Info, Session, $"HTTP {ID} {Session.RemoteEndPoint} request {SourceUrl} rewrite to {Url}"); + } } } diff --git a/src/HttpResponse.cs b/src/HttpResponse.cs index ec7374c..3557e34 100644 --- a/src/HttpResponse.cs +++ b/src/HttpResponse.cs @@ -13,7 +13,6 @@ public class HttpResponse : IDataResponse public HttpResponse() { Header = new Header(); - AsyncResult = false; } internal Newtonsoft.Json.JsonSerializer JsonSerializer { get; set; } @@ -40,45 +39,52 @@ public HttpResponse() public string RequestID { get; set; } - internal bool AsyncResult { get; set; } - private byte[] mLengthBuffer = new byte[10]; + public SameSiteType? SameSite { get; set; } + + public bool CookieSecure { get; set; } = false; + + public IList SetCookies => mSetCookies; + internal void Reset() { - AsyncResult = false; Header.Clear(); mSetCookies.Clear(); - //Header = new Header(); - //mSetCookies = new List(); mCompletedStatus = 0; mBody = null; Code = "200"; CodeMsg = "OK"; + if (Request.Server.Options.SameSite != null) + this.SameSite = Request.Server.Options.SameSite; + else + this.SameSite = null; + this.CookieSecure = Request.Server.Options.CookieSecure; } - public void Async() + public void SetCookie(string name, string value, string path, DateTime? expires = null) { - AsyncResult = true; + SetCookie(name, value, path, null, expires); } - public void SetCookie(string name, string value, string path, DateTime? expires = null) + public void SetCookie(string name, string value, DateTime? expires = null) { - SetCookie(name, value, path, null, expires); + SetCookie(name, value, "/", null, expires); } - public void SetCookie(string name, string value, DateTime? expires = null) - { - SetCookie(name, value, "/", null, expires); - } + [ThreadStatic] + static StringBuilder mCookerSB; - public void SetCookie(string name, string value, string path, string domain, DateTime? expires = null) + public void SetCookie(string name, string value, string path, string domain, DateTime? expires = null) { if (string.IsNullOrEmpty(name)) return; name = System.Web.HttpUtility.UrlEncode(name); value = System.Web.HttpUtility.UrlEncode(value); - StringBuilder sb = new StringBuilder(); + if (mCookerSB == null) + mCookerSB = new StringBuilder(); + mCookerSB.Clear(); + StringBuilder sb = mCookerSB; sb.Append(name).Append("=").Append(value); if (!string.IsNullOrEmpty(path)) @@ -95,6 +101,10 @@ public void SetCookie(string name, string value, string path, string domain, Dat } sb.Append(";HttpOnly"); + if (SameSite != null) + sb.Append(";SameSite=" + Enum.GetName(typeof(SameSiteType), this.SameSite.Value)); + if (CookieSecure) + sb.Append(";Secure"); mSetCookies.Add(sb.ToString()); } @@ -104,6 +114,15 @@ public void Result(object data) { Completed(data); } + else if (data is ValueTuple dataBuff) + { + Completed(new BinaryResult(new ArraySegment(dataBuff.Item1, 0, dataBuff.Item1.Length), dataBuff.Item2)); + } + else if (data is ValueTuple, string> dataBuff1) + { + Completed(new BinaryResult(dataBuff1.Item1, dataBuff1.Item2)); + + } else if (data is IResult) { Completed(data); @@ -185,7 +204,8 @@ private void OnWrite(PipeStream stream) hlen++; stream.Write(buffer, 0, hlen); - stream.Write(HeaderTypeFactory.SERVAR_HEADER_BYTES, 0, HeaderTypeFactory.SERVAR_HEADER_BYTES.Length); + if (Request.Server.Options.OutputServerTag) + stream.Write(HeaderTypeFactory.SERVAR_HEADER_BYTES, 0, HeaderTypeFactory.SERVAR_HEADER_BYTES.Length); Header.Write(stream); if (result != null) { @@ -266,7 +286,7 @@ void IDataResponse.Write(PipeStream stream) HttpApiServer server = Request.Server; if (server.EnableLog(EventArgs.LogType.Error)) { - server.Log(EventArgs.LogType.Error, $"{Request.RemoteIPAddress} {Request.Method} {Request.Url} response write data error {e_.Message}@{e_.StackTrace}"); + server.Log(EventArgs.LogType.Error, Request.Session, $"{Request.RemoteIPAddress} {Request.Method} {Request.Url} response write data error {e_.Message}@{e_.StackTrace}"); Request.Session.Dispose(); } } @@ -290,5 +310,17 @@ public override string ToString() return sb.ToString(); } + public void InnerError(string code, string message, bool outputStackTrace = false) + { + InnerError(code, message, null, outputStackTrace); + } + public void InnerError(string message, Exception e, bool outputStackTrace) + { + InnerError("500", message, e, outputStackTrace); + } + public void InnerError(string code, string message, Exception e, bool outputStackTrace) + { + Request?.Server?.OnInnerError(this, code, message, e, outputStackTrace); + } } } diff --git a/src/HttpToken.cs b/src/HttpToken.cs index a1bcc29..98d9ae1 100644 --- a/src/HttpToken.cs +++ b/src/HttpToken.cs @@ -1,6 +1,10 @@ -using BeetleX.EventArgs; +using BeetleX.Buffers; +using BeetleX.EventArgs; +using BeetleX.FastHttpApi.WebSockets; +using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.IO; using System.Text; namespace BeetleX.FastHttpApi @@ -29,5 +33,15 @@ public HttpToken() internal bool FirstRequest { get; set; } public DateTime CreateTime { get; internal set; } + + public PipeStream WebSocketData { get; set; } + + public Newtonsoft.Json.JsonSerializer WebSocketJsonSerializer { get; set; } + + internal RpsLimit WSRpsLimit { get; set; } + + internal RpsLimit HttpRpsLimit { get; set; } + + internal DataPacketType WSLastPacketType { get; set; } } } diff --git a/src/IActionContextParameter.cs b/src/IActionContextParameter.cs new file mode 100644 index 0000000..aebda7f --- /dev/null +++ b/src/IActionContextParameter.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + public interface IActionParameter : IDisposable + { + ActionContext Context { get; set; } + void Init(IHttpContext context); + } + +} diff --git a/src/IActionResultHandler.cs b/src/IActionResultHandler.cs index ff1e70d..3859d5b 100644 --- a/src/IActionResultHandler.cs +++ b/src/IActionResultHandler.cs @@ -9,9 +9,11 @@ public interface IActionResultHandler { void Success(object result); void Error(Exception e_, EventArgs.LogType logType = EventArgs.LogType.Error, int code = 500); + } - struct WSActionResultHandler : IActionResultHandler + + class WSActionResultHandler : IActionResultHandler { public WSActionResultHandler(WebsocketJsonContext jsonContext, HttpApiServer server, HttpRequest request, ActionResult result, WebSockets.DataFrame dataFrame, long startTime) @@ -24,6 +26,8 @@ public WSActionResultHandler(WebsocketJsonContext jsonContext, HttpApiServer ser StartTime = startTime; } + public ActionHandler ActionHandler { get; set; } + public WebsocketJsonContext DataContext; public HttpApiServer Server; @@ -36,8 +40,10 @@ public WSActionResultHandler(WebsocketJsonContext jsonContext, HttpApiServer ser public void Error(Exception e_, EventArgs.LogType logType = EventArgs.LogType.Error, int code = 500) { + + Server.OnActionExecutedError(DataContext, ActionHandler, e_, code, StartTime); if (Server.EnableLog(logType)) - Server.Log(logType, $"Websocket {Request.ID} {Request.RemoteIPAddress} execute {DataContext.ActionUrl} inner error {e_.Message}@{e_.StackTrace}"); + Server.Log(logType, Request.Session, $"Websocket {Request.ID} {Request.RemoteIPAddress} execute {DataContext.ActionUrl} inner error {e_.Message}@{e_.StackTrace}"); Result.Code = code; Result.Error = e_.Message; if (Server.Options.OutputStackTrace) @@ -51,37 +57,49 @@ public void Error(Exception e_, EventArgs.LogType logType = EventArgs.LogType.Er public void Success(object result) { - if (result is ActionResult) + Server.OnActionExecutedSuccess(DataContext, ActionHandler, StartTime); + var data = Request.Server.WSActionResultHandler?.Invoke(Request, ActionHandler, result); + if (data != null) { - Result = (ActionResult)result; - Result.ID = DataContext.RequestID; - if (Result.Url == null) - Result.Url = DataContext.ActionUrl; - DataFrame.Body = Result; + DataFrame.Body = data; } else { - Result.Data = result; + if (result is ActionResult) + { + Result = (ActionResult)result; + Result.ID = DataContext.RequestID; + if (Result.Url == null) + Result.Url = DataContext.ActionUrl; + DataFrame.Body = Result; + } + else + { + Result.Data = result; + } } DataFrame.Send(Request.Session, false); if (Server.EnableLog(EventArgs.LogType.Info)) - Server.Log(EventArgs.LogType.Info, $"Websocket {Request.ID} {Request.RemoteIPAddress} execute {DataContext.ActionUrl} action use time:{ Server.BaseServer.GetRunTime() - StartTime}ms"); + Server.Log(EventArgs.LogType.Info, Request.Session, $"Websocket {Request.ID} {Request.RemoteIPAddress} execute {DataContext.ActionUrl} action use time:{ Server.BaseServer.GetRunTime() - StartTime}ms"); } } - struct HttpActionResultHandler : IActionResultHandler + class HttpActionResultHandler : IActionResultHandler { - public HttpActionResultHandler(HttpApiServer server, HttpRequest request, HttpResponse response, long startTime) + public HttpActionResultHandler(HttpContext context, HttpApiServer server, HttpRequest request, HttpResponse response, long startTime) { Server = server; Request = request; Response = response; StartTime = startTime; + Context = context; } + public HttpContext Context; + public HttpApiServer Server; public HttpRequest Request; @@ -92,16 +110,16 @@ public HttpActionResultHandler(HttpApiServer server, HttpRequest request, HttpRe public void Error(Exception e_, EventArgs.LogType logType = EventArgs.LogType.Error, int code = 500) { + Server.OnActionExecutedError(Context, Request.ActionHandler, e_, code, StartTime); if (Server.EnableLog(logType)) - Server.Log(logType, + Server.Log(logType, Request.Session, $"HTTP {Request.ID} {Request.RemoteIPAddress} {Request.Method} { Request.Url} inner error {e_.Message}@{e_.StackTrace}"); - InnerErrorResult result = new InnerErrorResult($"http execute {Request.BaseUrl} error ", e_, Server.Options.OutputStackTrace); - result.Code = code.ToString(); - Response.Result(result); + Response.InnerError(code.ToString(), $"http execute {Request.BaseUrl} inner error!", e_, Server.Options.OutputStackTrace); } public void Success(object result) { + Server.OnActionExecutedSuccess(Context, Request.ActionHandler, StartTime); if (Server.EnableLog(EventArgs.LogType.Info)) Server.BaseServer.Log(EventArgs.LogType.Info, Request.Session, $"HTTP {Request.ID} {Request.RemoteIPAddress} {Request.Method} {Request.BaseUrl} use time:{Server.BaseServer.GetRunTime() - StartTime}ms"); diff --git a/src/IDGenerator.cs b/src/IDGenerator.cs new file mode 100644 index 0000000..e6e9dc7 --- /dev/null +++ b/src/IDGenerator.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + //1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 + //[组 256 ] [2010-1-1到当前时间秒 max:68719476735] [自增值 最大:1000000] + // [可用年数:2179] + public class IDGenerator + { + + private byte mGroup = 1; + + private long mSeconds; + + private ulong mID = 1; + + private long mLastTime; + + private System.Diagnostics.Stopwatch mWatch = new System.Diagnostics.Stopwatch(); + + public IDGenerator() + { + LoadIPAddress(); + Init(); + } + + public static IDGenerator Default { get; set; } = new IDGenerator(); + + public IDGenerator(byte group) + { + mGroup = group; + Init(); + } + private void LoadIPAddress() + { + var host = Dns.GetHostEntry(Dns.GetHostName()); + foreach (var ip in host.AddressList) + { + if (ip.AddressFamily == AddressFamily.InterNetwork) + { + var value = ip.GetAddressBytes(); + if (value[0] == 10) + { + mGroup = value[3]; + break; + + } + if (value[0] == 172) + { + if (value[1] >= 16 && value[1] <= 31) + { + mGroup = value[3]; + break; + } + } + if (value[0] == 192 && value[1] == 168) + { + mGroup = value[3]; + break; + } + } + } + if (mGroup == 0) + { + mGroup = 1; + } + } + + private void Init() + { + var ts = DateTime.Now - DateTime.Parse("2010-1-1"); + mSeconds = (long)ts.TotalSeconds; + mWatch.Restart(); + mLastTime = (long)Math.Floor(mWatch.Elapsed.TotalSeconds); + } + + public ulong Next() + { + lock (this) + { + ulong result = 0; + result |= (ulong)mGroup << 56; + mID++; + START: + var now = (long)Math.Floor(mWatch.Elapsed.TotalSeconds); + if (now - mLastTime > 1) + { + mID = 1; + mLastTime = now; + } + if (mID > 1000000) + { + System.Threading.Thread.Sleep(50); + goto START; + } + result |= (ulong)(mSeconds + mLastTime) << 20; + result |= mID; + return result; + } + } + } +} diff --git a/src/IDataContext.cs b/src/IDataContext.cs index af6d106..fbaf0d4 100644 --- a/src/IDataContext.cs +++ b/src/IDataContext.cs @@ -140,10 +140,6 @@ public void Result(object data) Response.Result(data); } - public void Async() - { - Response.Async(); - } public void SendToWebSocket(WebSockets.DataFrame data, HttpRequest request) { diff --git a/src/IHttpContext.cs b/src/IHttpContext.cs index b90e52a..11eaf10 100644 --- a/src/IHttpContext.cs +++ b/src/IHttpContext.cs @@ -29,7 +29,7 @@ public interface IHttpContext void SendToWebSocket(WebSockets.DataFrame data, Func filter = null); - void Async(); + // void Async(); bool WebSocket { get; } diff --git a/src/IPLimit.cs b/src/IPLimit.cs index 353ab6b..2bfa6d8 100644 --- a/src/IPLimit.cs +++ b/src/IPLimit.cs @@ -3,6 +3,7 @@ using System.Text; using System.Collections.Concurrent; using System.Linq; +using BeetleX.EventArgs; namespace BeetleX.FastHttpApi { @@ -12,6 +13,7 @@ public IPLimit(HttpApiServer server) { mHttpServer = server; mClearTimer = new System.Threading.Timer(OnClearLimit, null, 1000, 1000 * 600); + } private System.Threading.Timer mClearTimer; @@ -20,6 +22,27 @@ public IPLimit(HttpApiServer server) private ConcurrentDictionary mIpLimitTable = new ConcurrentDictionary(); + private ConcurrentDictionary mCustomIpLimitTable = new ConcurrentDictionary(); + + public IPLimitConfig Config { get; set; } + + + public void Load() + { + Config = IPLimitConfig.Instance; + Config.Save(); + foreach (var item in Config.Items) + { + AddIPAddress(item.IP, item.MaxRPS); + } + } + + public void Save() + { + Config.Items = (from a in mCustomIpLimitTable.Values select new LimitRecord { IP = a.IP, MaxRPS = a.MaxRPS }).ToList(); + Config.Save(); + } + public LimitItem GetItem(string ip) { LimitItem result = null; @@ -30,6 +53,17 @@ public LimitItem GetItem(string ip) return result; } + public void RemoveIPAddress(string ip) + { + mCustomIpLimitTable.TryRemove(ip, out LimitItem result); + } + + public void AddIPAddress(string ip, int maxrps) + { + var item = new LimitItem(ip, mHttpServer); + item.MaxRPS = maxrps; + mCustomIpLimitTable[ip] = item; + } private void OnClearLimit(object state) { try @@ -39,21 +73,23 @@ private void OnClearLimit(object state) { var now = TimeWatch.GetElapsedMilliseconds(); if (now - item.ActiveTime > 1000 * 600) - mIpLimitTable.TryRemove(item.IP,out LimitItem result); + mIpLimitTable.TryRemove(item.IP, out LimitItem result); } } - catch(Exception e_) + catch (Exception e_) { mHttpServer.GetLog(EventArgs.LogType.Error) - ?.Log(EventArgs.LogType.Error, $"IP limit clear error {e_.Message}@{e_.StackTrace}"); + ?.Log(EventArgs.LogType.Error, null, $"IP limit clear error {e_.Message}@{e_.StackTrace}"); } - + } private LimitItem GetOrCreateItem(HttpRequest request) { string ip = request.RemoteIPAddress; - if (mIpLimitTable.TryGetValue(ip, out LimitItem result)) + if (mCustomIpLimitTable.TryGetValue(ip, out LimitItem result)) + return result; + if (mIpLimitTable.TryGetValue(ip, out result)) return result; LimitItem item = new LimitItem(ip, mHttpServer); if (!mIpLimitTable.TryAdd(ip, item)) @@ -63,12 +99,27 @@ private LimitItem GetOrCreateItem(HttpRequest request) public bool ValidateRPS(HttpRequest request) { - if (mHttpServer.Options.IPRpsLimit == 0) + if (mHttpServer.Options.IPRpsLimit == 0 && mCustomIpLimitTable.Count == 0) return true; var item = GetOrCreateItem(request); return item.ValidateRPS(); } + + public class LimitRecord + { + + public string IP { get; set; } + + public int MaxRPS { get; set; } + } + + + public class IPLimitConfig : ConfigBase + { + public List Items { get; set; } = new List(); + } + public class LimitItem { #region rps limit @@ -87,6 +138,8 @@ public LimitItem(string ip, HttpApiServer server) private long mLastTime; + public int MaxRPS { get; set; } = 0; + public string IP { get; set; } public long ActiveTime { get; set; } @@ -101,7 +154,7 @@ public bool ValidateRPS() ActiveTime = now; if (mEnbaledTime > now) return false; - if (mServer.Options.IPRpsLimit == 0) + if (mServer.Options.IPRpsLimit == 0 && MaxRPS == 0) return true; if (now - mLastTime >= 1000) { @@ -111,9 +164,12 @@ public bool ValidateRPS() } else { + var max = System.Math.Min(mServer.Options.IPRpsLimit, MaxRPS); + if (max <= 0) + max = System.Math.Max(mServer.Options.IPRpsLimit, MaxRPS); mRPS++; - bool result = mRPS < mServer.Options.IPRpsLimit; - if (!result) + bool result = mRPS < max; + if (!result && mServer.Options.IPRpsLimitDisableTime > 0) mEnbaledTime = mServer.Options.IPRpsLimitDisableTime + now; return result; } @@ -124,5 +180,155 @@ public bool ValidateRPS() #endregion } + } + public class UrlLimitRecord + { + + public string Url { get; set; } + + public int MaxRPS { get; set; } + } + + public interface IUrlLimitConfig + { + List Items { get; set; } + + void Save(); + + T GetInstance(); + } + public class UrlLimitConfig : ConfigBase, IUrlLimitConfig + { + public List Items { get; set; } = new List(); + + + } + + public class DomainLimitConfig : ConfigBase, IUrlLimitConfig + { + public List Items { get; set; } = new List(); + + + } + + class UrlLimitAgent + { + public UrlLimitRecord Config { get; set; } + + public RpsLimit RpsLimit { get; set; } + + public long Version { get; set; } + public bool ValidateRPS() + { + if (Config == null) + return true; + return !RpsLimit.Check(Config.MaxRPS); + } + } + public class UrlLimit + where CONFIG : IUrlLimitConfig, new() + { + + private long mVersion = 0; + + private ConcurrentDictionary mLimitTable = + new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); + + private ConcurrentDictionary mCachedLimitTable = + new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); + + private UrlLimitRecord[] mUrlTables = new UrlLimitRecord[0]; + + public int CacheSize { get; set; } = 1024 * 200; + + public long Version + { + get + { + return mVersion; + } + } + + private void OnRefreshTable() + { + lock (this) + { + + var items = (from a in mLimitTable.Values + orderby a.Url.Length descending + select a).ToArray(); + if (items == null) + { + items = new UrlLimitRecord[0]; + } + mUrlTables = items; + System.Threading.Interlocked.Increment(ref mVersion); + } + } + + public int Count => mUrlTables.Length; + + public void AddUrl(string url, int maxrps) + { + mLimitTable[url] = new UrlLimitRecord { Url = url, MaxRPS = maxrps }; + OnRefreshTable(); + } + + public void RemoveUrl(string url) + { + mLimitTable.TryRemove(url, out UrlLimitRecord result); + OnRefreshTable(); + } + + public CONFIG Config { get; set; } + + internal UrlLimitAgent Match(string url,HttpRequest request) + { + if (mCachedLimitTable.TryGetValue(url, out UrlLimitAgent result)) + { + if (result.Version == Version) + return result; + } + var items = mUrlTables; + result = new UrlLimitAgent(); + foreach (var item in items) + { + if (url.IndexOf(item.Url, StringComparison.CurrentCultureIgnoreCase) >= 0) + { + result.RpsLimit = new RpsLimit(item.MaxRPS); + result.Config = item; + } + } + + result.Version = System.Threading.Interlocked.Add(ref mVersion, 0); + if (mCachedLimitTable.Count < this.CacheSize) + { + mCachedLimitTable[url] = result; + } + else + { + request.Server.GetLog(LogType.Warring)?.Log(LogType.Warring, null, $"Http url limit cached out of {this.CacheSize} size!"); + } + + return result; + } + public void Load() + { + + Config = new CONFIG().GetInstance(); + Config.Save(); + foreach (var item in Config.Items) + { + AddUrl(item.Url, item.MaxRPS); + } + } + + public void Save() + { + Config.Items = mLimitTable.Values.ToList(); + Config.Save(); + } + + } } diff --git a/src/IPv4Tables.cs b/src/IPv4Tables.cs index c10a1f9..17f0d5d 100644 --- a/src/IPv4Tables.cs +++ b/src/IPv4Tables.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text; namespace BeetleX.FastHttpApi @@ -40,7 +41,8 @@ public void AddWhite(params string[] ips) var item = IPv4Match.GetIpMatch(ip); if (item != null) { - mWhiteList.Add(item); + if (!mWhiteList.Contains(item)) + mWhiteList.Add(item); } } Reload(); @@ -53,7 +55,8 @@ public void AddBlack(params string[] ips) var item = IPv4Match.GetIpMatch(ip); if (item != null) { - mBlackList.Add(item); + if (!mBlackList.Contains(item)) + mBlackList.Add(item); } } Reload(); @@ -122,32 +125,39 @@ public void Load() } } - public bool Verify(System.Net.IPAddress ipaddress) + public bool Verify(string ipvalue) { if (Type == VerifyType.None) return true; - if (!ipaddress.IsIPv4MappedToIPv6) + if (Type == VerifyType.Black) { - if (ipaddress.GetAddressBytes().Length > 4) + if (IPAddress.TryParse(ipvalue, out IPAddress ipaddress)) + { + IPv4Match[] items = mBlackMatchs; + foreach (var item in items) + if (item.Match(ipaddress, false)) + return false; + return true; + } + else { return true; } - } - if (Type == VerifyType.Black) - { - IPv4Match[] items = mBlackMatchs; - foreach (var item in items) - if (item.Match(ipaddress)) - return false; - return true; } else { - IPv4Match[] items = mWhiteMatchs; - foreach (var item in items) - if (item.Match(ipaddress)) - return true; - return false; + if (IPAddress.TryParse(ipvalue, out IPAddress ipaddress)) + { + IPv4Match[] items = mWhiteMatchs; + foreach (var item in items) + if (item.Match(ipaddress)) + return true; + return false; + } + else + { + return false; + } } } @@ -185,6 +195,11 @@ public IPv4Match(string source, int ip, int? mark) public string Source { get; set; } + public override bool Equals(object obj) + { + return this.Source == ((IPv4Match)obj).Source; + } + public static IPv4Match GetIpMatch(string ip) { string[] values = ip.Split('/'); @@ -221,21 +236,13 @@ private static int GetIP(Span data) private uint? Mark; - public bool Match(System.Net.IPAddress remote) + public bool Match(System.Net.IPAddress remote, bool whiteList = true) { - var bytes = remote.GetAddressBytes(); + var bytes = remote.MapToIPv4().GetAddressBytes(); int ipdata; - - if (remote.IsIPv4MappedToIPv6) - { - ipdata = GetIP(bytes.AsSpan().Slice(bytes.Length - 4)); - } - else - { - ipdata = GetIP(bytes); - } + ipdata = GetIP(bytes); if (ipdata == 1 || ipdata == 0) - return true; + return whiteList; if (Mark == null) return IPValue == ipdata; else diff --git a/src/IResult.cs b/src/IResult.cs index 3222ab1..6f4adff 100644 --- a/src/IResult.cs +++ b/src/IResult.cs @@ -132,7 +132,16 @@ public InnerErrorResult(string code, string message, Exception e, bool outputSta { Code = code; Message = message; - Error = e.Message; + if (e != null) + { + if (e.InnerException == null) + Error = e.Message; + else + { + Error = $"{e.Message} ({e.InnerException.Message})"; + } + } + if (outputStackTrace) SourceCode = e.StackTrace; else @@ -159,7 +168,7 @@ public override void Setting(HttpResponse response) public override void Write(PipeStream stream, HttpResponse response) { - stream.WriteLine(Message); + // stream.WriteLine(Message); if (!string.IsNullOrEmpty(Error)) { stream.WriteLine(Error); @@ -221,9 +230,33 @@ public override void Write(PipeStream stream, HttpResponse response) } } - public class UpgradeWebsocketResult : ResultBase + + public class UpgradeWebsocketError : ResultBase { - public UpgradeWebsocketResult(string websocketKey) + + public UpgradeWebsocketError(int code, string msg) + { + Code = code; + CodeMsg = msg; + } + + public override void Setting(HttpResponse response) + { + response.Code = Code.ToString(); + response.CodeMsg = CodeMsg; + } + + public int Code { get; set; } + + public string CodeMsg { get; set; } + + public override bool HasBody => false; + } + + + public class UpgradeWebsocketSuccess : ResultBase + { + public UpgradeWebsocketSuccess(string websocketKey) { WebsocketKey = websocketKey; } @@ -250,18 +283,56 @@ public override void Setting(HttpResponse response) public class TextResult : ResultBase { - public TextResult(string text) + public TextResult(string text, bool autoGzip = false) { Text = text == null ? "" : text; + mAutoGzip = autoGzip; } + private bool mAutoGzip = false; + public string Text { get; set; } public override bool HasBody => true; + private ArraySegment? mGzipData; + + public override void Setting(HttpResponse response) + { + base.Setting(response); + if (mAutoGzip && Text.Length > 1024) + { + var buffer = System.Buffers.ArrayPool.Shared.Rent(Text.Length * 6); + var len = Encoding.UTF8.GetBytes(Text, buffer); + mGzipData = new ArraySegment(buffer, 0, len); + response.Header.Add("Content-Encoding", "gzip"); + } + } + public override void Write(PipeStream stream, HttpResponse response) { - stream.Write(Text); + if (mGzipData != null) + { + try + { + using (stream.LockFree()) + { + using (var gzipStream = new GZipStream(stream, CompressionMode.Compress, true)) + { + gzipStream.Write(mGzipData.Value.Array, mGzipData.Value.Offset, mGzipData.Value.Count); + gzipStream.Flush(); + } + } + } + finally + { + System.Buffers.ArrayPool.Shared.Return(mGzipData.Value.Array); + } + } + else + { + stream.Write(Text); + } } } @@ -271,8 +342,7 @@ public JsonResult(object data, bool autoGzip = false) { Data = data; mAutoGzip = autoGzip; - if (autoGzip) - OnSerialize(); + } public object Data { get; set; } @@ -284,12 +354,12 @@ public JsonResult(object data, bool autoGzip = false) [ThreadStatic] private static System.Text.StringBuilder mJsonText; - private void OnSerialize() + private void OnSerialize(HttpResponse response) { if (mJsonText == null) mJsonText = new System.Text.StringBuilder(); mJsonText.Clear(); - JsonSerializer serializer = new JsonSerializer(); + JsonSerializer serializer = response.JsonSerializer; System.IO.StringWriter writer = new System.IO.StringWriter(mJsonText); JsonTextWriter jsonTextWriter = new JsonTextWriter(writer); serializer.Serialize(jsonTextWriter, Data); @@ -310,6 +380,8 @@ private void OnSerialize() public override void Setting(HttpResponse response) { base.Setting(response); + if (this.mAutoGzip) + OnSerialize(response); if (mAutoGzip && mJsonData.Count > 1024 * 2) { response.Header.Add("Content-Encoding", "gzip"); @@ -453,6 +525,60 @@ public FileResult(string file, string contentType, bool gzip = false) public bool GZip { get; set; } = false; } + + public class BinaryResult : BeetleX.FastHttpApi.IResult + { + public BinaryResult(ArraySegment data, string contentType = null) + { + if (!string.IsNullOrEmpty(contentType)) + { + mContentType = new ContentType(contentType); + } + Data = data; + } + + private IHeaderItem mContentType = ContentTypes.OCTET_STREAM; + + public IHeaderItem ContentType => mContentType; + + public int Length { get; set; } + + public bool AutoGZIP { get; set; } = false; + + public bool HasBody => true; + + public ArraySegment Data { get; private set; } + + public Action Completed { get; set; } + + public virtual void Setting(HttpResponse response) + { + + } + + public virtual void Write(PipeStream stream, HttpResponse response) + { + if (AutoGZIP) + { + using (stream.LockFree()) + { + using (var gzipStream = new GZipStream(stream, CompressionMode.Compress, true)) + { + gzipStream.Write(Data.Array, Data.Offset, Data.Count); + gzipStream.Flush(); + } + } + } + else + { + stream.Write(Data.Array, Data.Offset, Data.Count); + } + Completed?.Invoke(response, this); + } + } + + + public class DownLoadResult : BeetleX.FastHttpApi.IResult { public DownLoadResult(string text, string fileName, IHeaderItem contentType = null) diff --git a/src/IRpsLimitHandler.cs b/src/IRpsLimitHandler.cs new file mode 100644 index 0000000..ce41d5e --- /dev/null +++ b/src/IRpsLimitHandler.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + public interface IRpsLimitHandler + { + string ID { get; set; } + + string Name { get; set; } + + bool Check(HttpRequest request, HttpResponse response); + } +} diff --git a/src/ModuleManage.cs b/src/ModuleManage.cs index a81d5ef..4835e69 100644 --- a/src/ModuleManage.cs +++ b/src/ModuleManage.cs @@ -69,11 +69,11 @@ private void OnUpdateHandler(object state) try { Load(item.Module); - Server.Log(EventArgs.LogType.Info, $"{item.Module} upgrades success"); + Server.Log(EventArgs.LogType.Info,null, $"{item.Module} upgrades success"); } catch (Exception e_) { - Server.Log(EventArgs.LogType.Error, $"{item.Module} upgrades error {e_.Message} {e_.StackTrace}"); + Server.Log(EventArgs.LogType.Error,null, $"{item.Module} upgrades error {e_.Message} {e_.StackTrace}"); } finally { @@ -85,7 +85,7 @@ private void OnUpdateHandler(object state) } catch (Exception e_) { - Server.Log(EventArgs.LogType.Error, $"upgrades module error {e_.Message} {e_.StackTrace}"); + Server.Log(EventArgs.LogType.Error,null, $"upgrades module error {e_.Message} {e_.StackTrace}"); } finally { @@ -108,7 +108,7 @@ private void OnFileWatchHandler(object sender, FileSystemEventArgs e) item.FullName = e.FullPath; item.Time = Server.BaseServer.GetRunTime(); mUpdateItems[e.Name] = item; - Server.Log(EventArgs.LogType.Info, $"upload {e.Name} module"); + Server.Log(EventArgs.LogType.Info,null, $"upload {e.Name} module"); } } @@ -137,7 +137,7 @@ private void ClearFiles() } catch (Exception e_) { - Server.Log(EventArgs.LogType.Error, $"clear files error {e_.Message}"); + Server.Log(EventArgs.LogType.Error,null, $"clear files error {e_.Message}"); } } @@ -163,14 +163,14 @@ private void OnLoadAssembly(IList files, int count) System.Reflection.Assembly assembly = System.Reflection.Assembly.LoadFile(file); Server.ResourceCenter.LoadManifestResource(assembly); Server.ActionFactory.Register(assembly); - Server.Log(EventArgs.LogType.Info, $"loaded {aname} assembly success"); + Server.Log(EventArgs.LogType.Info,null, $"loaded {aname} assembly success"); OnAssemblyLoding(new EventAssemblyLoadingArgs(assembly)); success.Add(file); } catch (Exception e_) { - Server.Log(EventArgs.LogType.Error, $"load {aname} assembly error {e_.Message} {e_.StackTrace}"); + Server.Log(EventArgs.LogType.Error,null, $"load {aname} assembly error {e_.Message} {e_.StackTrace}"); } } } @@ -210,7 +210,7 @@ public void Load(string module) mUpdateCount++; if (mUpdateCount >= 1000) mUpdateCount = 0; - Server.Log(EventArgs.LogType.Info, $"loding {module} module ..."); + Server.Log(EventArgs.LogType.Info,null, $"loding {module} module ..."); string zipfile = mPath + module + ".zip"; string target = mRunningPath + module + mUpdateCount.ToString("000") + System.IO.Path.DirectorySeparatorChar; if (System.IO.Directory.Exists(target)) @@ -232,16 +232,16 @@ public void Load(string module) files.Add(assemblyFile); } OnLoadAssembly(files, 0); - Server.Log(EventArgs.LogType.Info, $"loaded {module} module success"); + Server.Log(EventArgs.LogType.Info,null, $"loaded {module} module success"); } else { - Server.Log(EventArgs.LogType.Warring, $"{module} not found!"); + Server.Log(EventArgs.LogType.Warring,null, $"{module} not found!"); } } catch (Exception e_) { - Server.Log(EventArgs.LogType.Error, $"load {module} error {e_.Message} {e_.StackTrace}"); + Server.Log(EventArgs.LogType.Error,null, $"load {module} error {e_.Message} {e_.StackTrace}"); } } diff --git a/src/NotLoadResourceAttribute.cs b/src/NotLoadResourceAttribute.cs new file mode 100644 index 0000000..9c95da5 --- /dev/null +++ b/src/NotLoadResourceAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + [AttributeUsage(AttributeTargets.Assembly, Inherited = false)] + public class NotLoadResourceAttribute : Attribute + { + } +} diff --git a/src/OptionsAttribute.cs b/src/OptionsAttribute.cs index c91d931..276a75e 100644 --- a/src/OptionsAttribute.cs +++ b/src/OptionsAttribute.cs @@ -27,7 +27,7 @@ public OptionsAttribute() public string AllowMaxAge { get; set; } - public bool? AllowCredentials { get; set; } + public string AllowCredentials { get; set; } public string Vary { get; set; } = "Origin"; @@ -53,16 +53,9 @@ public virtual void Setting(HttpResponse response) { response.Header["Access-Control-Max-Age"] = AllowMaxAge; } - if (AllowCredentials != null) + if (!string.IsNullOrEmpty(AllowCredentials)) { - if (AllowCredentials.Value) - { - response.Header["Access-Control-Allow-Credentials"] = "true"; - } - else - { - response.Header["Access-Control-Allow-Credentials"] = "false"; - } + response.Header["Access-Control-Allow-Credentials"] = AllowCredentials; } } @@ -75,7 +68,7 @@ public virtual void SetResponse(HttpRequest request, HttpResponse response) { HttpApiServer server = request.Server; if (server.EnableLog(EventArgs.LogType.Debug)) - server.Log(EventArgs.LogType.Debug, $"{request.RemoteIPAddress} {request.Method} {request.Url} set options"); + server.Log(EventArgs.LogType.Debug, request.Session, $"{request.RemoteIPAddress} {request.Method} {request.Url} set options"); response.Header["Access-Control-Allow-Origin"] = AllowOrigin; } } diff --git a/src/PostAttribute.cs b/src/PostAttribute.cs index ff830d4..3e7da19 100644 --- a/src/PostAttribute.cs +++ b/src/PostAttribute.cs @@ -80,4 +80,16 @@ public RequestMaxRPS(int value) public int Value { get; set; } } + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class RouteMapAttribute : Attribute + { + + public RouteMapAttribute(string url) + { + Url = url; + } + + public string Url { get; set; } + } + } diff --git a/src/Properties/PublishProfiles/FolderProfile.pubxml b/src/Properties/PublishProfiles/FolderProfile.pubxml index 67a81d8..e8ff9a9 100644 --- a/src/Properties/PublishProfiles/FolderProfile.pubxml +++ b/src/Properties/PublishProfiles/FolderProfile.pubxml @@ -4,10 +4,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - FileSystem Release Any CPU - netcoreapp2.1 C:\localnuget + FileSystem \ No newline at end of file diff --git a/src/Properties/PublishProfiles/FolderProfile.pubxml.user b/src/Properties/PublishProfiles/FolderProfile.pubxml.user index 312c6e3..e792573 100644 --- a/src/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/src/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -3,4 +3,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> + + True|2022-11-21T11:20:42.1001202Z;True|2022-11-21T19:09:45.3036652+08:00;True|2022-11-17T10:32:49.8322910+08:00;True|2022-11-17T10:20:23.9129917+08:00;True|2022-11-16T15:10:03.5199946+08:00;True|2022-06-07T19:20:14.1057577+08:00;True|2022-06-01T13:06:13.5070879+08:00;True|2022-06-01T13:05:16.6689441+08:00;True|2022-05-27T19:39:09.3609245+08:00;True|2022-05-11T12:20:30.4528149+08:00;True|2022-05-11T12:11:51.0408354+08:00;True|2022-05-11T12:03:12.5969654+08:00;True|2022-04-30T14:59:44.4055442+08:00;True|2022-02-24T13:26:14.1536154+08:00;True|2022-02-24T13:25:15.1625893+08:00;True|2022-02-18T20:00:04.6422454+08:00;True|2022-02-14T19:52:18.5891643+08:00;True|2022-02-14T19:37:15.2862284+08:00;True|2022-02-11T15:17:52.3935173+08:00;True|2022-02-11T15:12:38.4651377+08:00;True|2022-02-08T19:14:53.8108823+08:00;True|2022-02-08T19:13:40.3788720+08:00;True|2022-02-08T18:47:15.5500865+08:00;True|2022-02-08T18:41:21.1176248+08:00;False|2022-02-08T18:39:14.6964646+08:00;True|2021-10-11T21:24:14.8886166+08:00;True|2021-10-10T12:59:53.7874198+08:00;True|2021-09-17T11:03:57.5658942+08:00;True|2021-09-16T22:16:49.9328108+08:00;True|2021-09-16T22:12:02.5285439+08:00;True|2021-09-16T21:22:44.7974152+08:00;True|2021-09-16T20:59:35.0137932+08:00;True|2021-09-06T20:21:21.0413421+08:00;True|2021-09-06T20:20:37.8011537+08:00; + \ No newline at end of file diff --git a/src/Route/RouteAttribute.cs b/src/Route/RouteAttribute.cs index eebe83f..58bab9a 100644 --- a/src/Route/RouteAttribute.cs +++ b/src/Route/RouteAttribute.cs @@ -32,4 +32,4 @@ public string Analysis(string parent) return parent + Templete; } } -} +} \ No newline at end of file diff --git a/src/Route/RouteGroup.cs b/src/Route/RouteGroup.cs index 9b8ca19..65bcd9a 100644 --- a/src/Route/RouteGroup.cs +++ b/src/Route/RouteGroup.cs @@ -5,10 +5,7 @@ namespace BeetleX.FastHttpApi { - - - - class RouteGroup + public class RouteGroup { public RouteGroup() diff --git a/src/Route/RouteRewrite.cs b/src/Route/RouteRewrite.cs index 47ca9e7..9309181 100644 --- a/src/Route/RouteRewrite.cs +++ b/src/Route/RouteRewrite.cs @@ -27,6 +27,8 @@ public RouteRewrite(HttpApiServer server) private ConcurrentDictionary mRoutes = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + public ConcurrentDictionary Routes => mRoutes; + private RouteGroup[] mMatchRoutes = new RouteGroup[0]; private HttpApiServer mServer; @@ -69,7 +71,7 @@ private void ChangeVersion() private void Add(UrlRoute item) { - mServer.GetLog(EventArgs.LogType.Info)?.Log(EventArgs.LogType.Info, $"HTTP set rewrite url [{item.Host}{item.Url}] to [{item.Rewrite}]"); + mServer.GetLog(EventArgs.LogType.Info)?.Log(EventArgs.LogType.Info, null, $"HTTP set rewrite url [{item.Host}{item.Url}] to [{item.Rewrite}]"); item.Init(); RouteGroup rg = null; mRoutes.TryGetValue(item.Path, out rg); @@ -90,7 +92,7 @@ public void Remove(string host, string url) item.Init(); if (mRoutes.TryGetValue(item.Path, out RouteGroup rg)) { - mServer.GetLog(EventArgs.LogType.Info)?.Log(EventArgs.LogType.Info, $"HTTP remove rewrite url {item.Url}"); + mServer.GetLog(EventArgs.LogType.Info)?.Log(EventArgs.LogType.Info, null, $"HTTP remove rewrite url {item.Url}"); rg.Remove(item); ChangeVersion(); } @@ -108,7 +110,9 @@ public RouteRewrite Add(string url, string rewriteurl) public RouteRewrite Add(string host, string url, string rewriteurl, string ext) { - var extTag = url.IndexOf("."); + if (rewriteurl[0] != '/') + rewriteurl = '/' + rewriteurl; + var extTag = url.LastIndexOf("."); if (extTag > 0) ext = url.Substring(extTag + 1, url.Length - extTag - 1); UrlRoute route = new UrlRoute { Rewrite = rewriteurl, Url = url, Ext = ext, Host = host }; @@ -134,7 +138,7 @@ public bool Match(HttpRequest request, out RouteMatchResult result, QueryString { if (request.Server.EnableLog(EventArgs.LogType.Info)) { - request.Server.Log(EventArgs.LogType.Info, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {key} rewrite cached miss"); + request.Server.Log(EventArgs.LogType.Info, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {key} rewrite cached miss"); } return false; } @@ -146,7 +150,7 @@ public bool Match(HttpRequest request, out RouteMatchResult result, QueryString result = cached.MatchResult; if (request.Server.EnableLog(EventArgs.LogType.Info)) { - request.Server.Log(EventArgs.LogType.Info, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {key} rewrite cached hit"); + request.Server.Log(EventArgs.LogType.Info, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {key} rewrite cached hit"); } return true; @@ -194,7 +198,7 @@ public bool Match(HttpRequest request, out RouteMatchResult result, QueryString UrlRouteAgent exits = (UrlRouteAgent)mRouteCached.ExistOrAdd(key, agent); if (request.Server.EnableLog(EventArgs.LogType.Info)) { - request.Server.Log(EventArgs.LogType.Info, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {key} rewrite save to cached"); + request.Server.Log(EventArgs.LogType.Info, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} {request.Method} {key} rewrite save to cached"); } if (exits != null) { diff --git a/src/Route/UrlRoute.cs b/src/Route/UrlRoute.cs index 84a326b..f47bbb1 100644 --- a/src/Route/UrlRoute.cs +++ b/src/Route/UrlRoute.cs @@ -66,7 +66,7 @@ public void Init() } // if (this.Prefix == null) Path = $"{Url.Substring(0, index + 1)}"; - ID = $"{Host}{Path}"; + ID = $"{Host}{Url}"; //else // Path = $"{this.Prefix.Value}{Url.Substring(0, index + 1)}"; Valid = Regex.IsMatch(Url, parent); diff --git a/src/RpsLimit.cs b/src/RpsLimit.cs new file mode 100644 index 0000000..3a1e916 --- /dev/null +++ b/src/RpsLimit.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + public class RpsLimit + { + public RpsLimit(int max) + { + mMax = max; + } + + private int mMax; + + private long mRpsCount; + + private long mLastRpsTime; + + public void SetMaxRpx(int value) + { + this.mMax = value; + } + + public bool Check(int max = 0) + { + if (max > 0) + mMax = max; + if (mMax <= 0) + return false; + else + { + mRpsCount = System.Threading.Interlocked.Increment(ref mRpsCount); + long now = TimeWatch.GetElapsedMilliseconds(); + long time = now - mLastRpsTime; + if (time >= 1000) + { + System.Threading.Interlocked.Exchange(ref mRpsCount, 0); + System.Threading.Interlocked.Exchange(ref mLastRpsTime, now); + } + else + { + if (mRpsCount > mMax) + return true; + } + } + return false; + } + } +} diff --git a/src/SameSiteType.cs b/src/SameSiteType.cs new file mode 100644 index 0000000..70e9bf0 --- /dev/null +++ b/src/SameSiteType.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + public enum SameSiteType + { + None, + Strict, + Lax + } +} diff --git a/src/ServerController.cs b/src/ServerController.cs index 58c29fc..d9cd819 100644 --- a/src/ServerController.cs +++ b/src/ServerController.cs @@ -101,7 +101,7 @@ public object __GetOptions(IHttpContext context) server.Options.SessionTimeOut, server.Options.MaxBodyLength, server.Options.LogLevel, - server.Options.WebSocketMaxRPS, + server.Options.WebSocketSessionMaxRps, server.Options.MaxConnections, server.Options.WriteLog, server.Options.LogToConsole, @@ -155,7 +155,7 @@ public void __SetOptions(ServerSetting body, IHttpContext context) server.Options.SessionTimeOut = body.SessionTimeOut; server.Options.MaxBodyLength = body.MaxBodyLength; server.Options.LogLevel = body.LogLevel; - server.Options.WebSocketMaxRPS = body.WebSocketMaxRPS; + server.Options.WebSocketSessionMaxRps = body.WebSocketMaxRPS; server.Options.MaxConnections = body.MaxConnections; server.Options.WriteLog = body.WriteLog; server.Options.LogToConsole = body.LogToConsole; diff --git a/src/ServerCounter.cs b/src/ServerCounter.cs index b90ea36..1490f0c 100644 --- a/src/ServerCounter.cs +++ b/src/ServerCounter.cs @@ -59,7 +59,12 @@ public ServerStatus Next(bool actionDetail = false) result.WebApiVersion = mServer.GetType().Assembly.GetName().Version.ToString(); result.ServerName = mServer.Name; result.Host = mServer.Options.Host; - result.Port = mServer.Options.Port; + if(!mServer.Options.SSLOnly) + result.Port = mServer.Options.Port.ToString(); + if(mServer.Options.SSL) + { + result.TLSPort = mServer.Options.SSLPort.ToString(); + } TimeSpan ts = (DateTime.Now - mServer.StartTime); result.RunTime = $"{(long)ts.Days}:{(long)ts.Hours}:{(long)ts.Minutes}:{(long)ts.Seconds}"; @@ -143,7 +148,9 @@ public ServerStatus() public string Host { get; set; } - public int Port { get; set; } + public string Port { get; set; } + + public string TLSPort { get; set; } public string RunTime { get; set; } diff --git a/src/StaticResurce/FileBlock.cs b/src/StaticResurce/FileBlock.cs index 2faf292..e0f43d3 100644 --- a/src/StaticResurce/FileBlock.cs +++ b/src/StaticResurce/FileBlock.cs @@ -65,15 +65,17 @@ void IDataResponse.Write(PipeStream stream) int len = stream.CacheLength; if (gZipStream == null) gZipStream = new GZipStream(stream, CompressionMode.Compress, true); - gZipStream.Write(Data.Array, Data.Offset, Data.Count); - gZipStream.Flush(); - if (Offset == mFileResource.Length) + using (stream.LockFree()) { - if (gZipStream != null) + gZipStream.Write(Data.Array, Data.Offset, Data.Count); + gZipStream.Flush(); + if (Offset == mFileResource.Length) { - using (stream.LockFree()) + if (gZipStream != null) { + gZipStream.Dispose(); + } } } diff --git a/src/StaticResurce/FileResource.cs b/src/StaticResurce/FileResource.cs index eaa4858..d76e326 100644 --- a/src/StaticResurce/FileResource.cs +++ b/src/StaticResurce/FileResource.cs @@ -77,20 +77,40 @@ protected virtual void LoadFile() byte[] buffer = HttpParse.GetByteBuffer(); if (length > 0) { - using (System.IO.MemoryStream memory = new MemoryStream()) + if (GZIP) { - using (GZipStream gstream = new GZipStream(memory, CompressionMode.Compress)) + using (System.IO.MemoryStream memory = new MemoryStream()) { + using (GZipStream gstream = new GZipStream(memory, CompressionMode.Compress)) + { + while (length > 0) + { + int len = fsstream.Read(buffer, 0, buffer.Length); + length -= len; + gstream.Write(buffer, 0, len); + gstream.Flush(); + if (length == 0) + Data = memory.ToArray(); + } + } + } + } + else + { + using (System.IO.MemoryStream memory = new MemoryStream()) + { + while (length > 0) { int len = fsstream.Read(buffer, 0, buffer.Length); length -= len; - gstream.Write(buffer, 0, len); - gstream.Flush(); + memory.Write(buffer, 0, len); + memory.Flush(); if (length == 0) Data = memory.ToArray(); } } + } } else @@ -117,7 +137,7 @@ public void Load() Length = (int)stream.Length; } } - if (Length < 1024 * 1024*5 && !string.IsNullOrEmpty(UrlName)) + if (Length < 1024 * 1024 * 5 && !string.IsNullOrEmpty(UrlName)) { FileMD5 = FMD5(FullName, this.Assembly); } diff --git a/src/StaticResurce/ResourceCenter.cs b/src/StaticResurce/ResourceCenter.cs index 95de3f3..d954755 100644 --- a/src/StaticResurce/ResourceCenter.cs +++ b/src/StaticResurce/ResourceCenter.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.IO; +using System.Linq; using System.Text; +using System.Threading.Tasks; namespace BeetleX.FastHttpApi.StaticResurce { @@ -42,11 +45,10 @@ public void SetDefaultPages(string files) } } - public void SetFileExts(string exts) + public string SetFileExts(string exts) { if (exts != null) { - Server.Options.StaticResurceType = exts; foreach (string item in exts.ToLower().Split(';')) { if (!mExts.ContainsKey(item)) @@ -55,7 +57,10 @@ public void SetFileExts(string exts) mExts[fct.Ext] = fct; } } + Server.Options.StaticResurceType = string.Join(";", mExts.Keys.ToArray()); + } + return Server.Options.StaticResurceType; } private ConcurrentDictionary mResources = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -189,7 +194,16 @@ public void Load() fsw.IncludeSubdirectories = true; fsw.Changed += (o, e) => { - CreateResource(e.FullPath, true); + Task.Run(async () => + { + try + { + await Task.Delay(500); + CreateResource(e.FullPath, true); + } + catch { } + }); + }; fsw.EnableRaisingEvents = true; mFileWatch.Add(fsw); @@ -198,17 +212,23 @@ public void Load() } + } + + private void OnCrossDomain(HttpResponse response) + { + } private void OutputFileResource(FileContentType fct, FileResource fr, HttpResponse response) { + OnCrossDomain(response); if (!Debug) { string IfNoneMatch = response.Request.IfNoneMatch; if (!string.IsNullOrEmpty(IfNoneMatch) && IfNoneMatch == fr.FileMD5) { if (Server.EnableLog(EventArgs.LogType.Info)) - Server.BaseServer.Log(EventArgs.LogType.Info, null, $"HTTP {response.Request.ID} {response.Request.RemoteIPAddress} get {response.Request.Url} source no modify "); + Server.BaseServer.Log(EventArgs.LogType.Info, response.Request.Session, $"HTTP {response.Request.ID} {response.Request.RemoteIPAddress} get {response.Request.Url} source no modify "); if (Server.Options.StaticResurceCacheTime > 0) { response.Header.Add(HeaderTypeFactory.CACHE_CONTROL, "public, max-age=" + Server.Options.StaticResurceCacheTime); @@ -243,7 +263,7 @@ private void OutputFileResource(FileContentType fct, FileResource fr, HttpRespon SetChunked(response); if (Server.EnableLog(EventArgs.LogType.Info)) { - Server.BaseServer.Log(EventArgs.LogType.Info, null, $"HTTP {response.Request.ID} {response.Request.RemoteIPAddress} get {response.Request.BaseUrl} response gzip {fr.GZIP}"); + Server.BaseServer.Log(EventArgs.LogType.Info, response.Request.Session, $"HTTP {response.Request.ID} {response.Request.RemoteIPAddress} get {response.Request.BaseUrl} response gzip {fr.GZIP}"); } HttpToken token = (HttpToken)response.Session.Tag; token.File = fr.CreateFileBlock(); @@ -255,6 +275,7 @@ private void OutputFileResource(FileContentType fct, FileResource fr, HttpRespon public void OutputFile(FileResult result, HttpRequest request, HttpResponse response) { + OnCrossDomain(response); var file = result.File; if (file.IndexOf(System.IO.Path.DirectorySeparatorChar) == -1) { @@ -310,7 +331,7 @@ public void OutputFile(FileResult result, HttpRequest request, HttpResponse resp SetChunked(response); if (Server.EnableLog(EventArgs.LogType.Info)) { - Server.BaseServer.Log(EventArgs.LogType.Info, null, $"HTTP {response.Request.ID} {response.Request.RemoteIPAddress} get {response.Request.BaseUrl} response gzip {efra.Resource.GZIP}"); + Server.BaseServer.Log(EventArgs.LogType.Info, request.Session, $"HTTP {response.Request.ID} {response.Request.RemoteIPAddress} get {response.Request.BaseUrl} response gzip {efra.Resource.GZIP}"); } HttpToken token = (HttpToken)response.Session.Tag; token.File = efra.Resource.CreateFileBlock(); @@ -336,21 +357,12 @@ public void ProcessFile(HttpRequest request, HttpResponse response) FileContentType fct = null; FileResource fr = null; string url = request.BaseUrl; - var result = MatchVirtuslFolder(request.BaseUrl); - if (result != null) - { - FileResult fileResult = new FileResult(result); - OutputFile(fileResult, request, response); - return; - } - if (url[url.Length - 1] == '/') { for (int i = 0; i < mDefaultPages.Count; i++) { string defaultpage = url + mDefaultPages[i]; string ext = HttpParse.GetBaseUrlExt(defaultpage); - if (!mExts.TryGetValue(ext, out fct)) { continue; @@ -362,8 +374,28 @@ public void ProcessFile(HttpRequest request, HttpResponse response) return; } } + string result = null; + + for (int i = 0; i < mDefaultPages.Count; i++) + { + string defaultpage = mDefaultPages[i]; + result = MatchVirtuslFolder(url + defaultpage); + if (result != null) + break; + } + if (result != null) + { + if (result != null) + { + if (Server.EnableLog(EventArgs.LogType.Info)) + Server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} get {request.Url} file from {result}"); + FileResult fileResult = new FileResult(result); + OutputFile(fileResult, request, response); + return; + } + } if (Server.EnableLog(EventArgs.LogType.Warring)) - Server.BaseServer.Log(EventArgs.LogType.Warring, null, $"HTTP {request.ID} {request.RemoteIPAddress} get {request.Url} file not found"); + Server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} get {request.Url} file not found"); if (!Server.OnHttpRequesNotfound(request, response).Cancel) { NotFoundResult notFound = new NotFoundResult("{0} file not found", request.Url); @@ -372,10 +404,11 @@ public void ProcessFile(HttpRequest request, HttpResponse response) return; } if (ExtSupport(request.Ext)) - { + { url = System.Net.WebUtility.UrlDecode(url); fct = mExts[request.Ext]; fr = GetFileResource(url); + var resourceData = fr; if (!Server.Options.Debug && fr != null) { OutputFileResource(fct, fr, response); @@ -383,7 +416,7 @@ public void ProcessFile(HttpRequest request, HttpResponse response) else { string file; - string fileurl = HttpParse.GetBaseUrl(request.Url); + string fileurl = HttpParse.GetBaseUrl(url); fileurl = System.Net.WebUtility.UrlDecode(fileurl); if (ExistsFile(request, fileurl, out file)) { @@ -396,12 +429,30 @@ public void ProcessFile(HttpRequest request, HttpResponse response) } else { - if (Server.EnableLog(EventArgs.LogType.Warring)) - Server.BaseServer.Log(EventArgs.LogType.Warring, null, $"HTTP {request.ID} {request.RemoteIPAddress} get {request.Url} file not found"); - if (!Server.OnHttpRequesNotfound(request, response).Cancel) + fileurl = HttpParse.GetBaseUrl(url); + string result = null; + result = MatchVirtuslFolder(fileurl); + if (result != null) { - NotFoundResult notFound = new NotFoundResult("{0} file not found", request.Url); - response.Result(notFound); + if (Server.EnableLog(EventArgs.LogType.Info)) + Server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} get {request.Url} file from {result}"); + FileResult fileResult = new FileResult(result); + OutputFile(fileResult, request, response); + return; + } + if (resourceData != null) + { + OutputFileResource(fct, resourceData, response); + } + else + { + if (Server.EnableLog(EventArgs.LogType.Warring)) + Server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} get {request.Url} file not found"); + if (!Server.OnHttpRequesNotfound(request, response).Cancel) + { + NotFoundResult notFound = new NotFoundResult("{0} file not found", request.Url); + response.Result(notFound); + } } } @@ -411,9 +462,10 @@ public void ProcessFile(HttpRequest request, HttpResponse response) { if (Server.EnableLog(EventArgs.LogType.Warring)) - Server.BaseServer.Log(EventArgs.LogType.Warring, null, $"HTTP {request.ID} {request.RemoteIPAddress} get { request.BaseUrl} file ext not support"); - NotSupportResult notSupport = new NotSupportResult("get {0} file {1} ext not support", request.Url, request.Ext); - response.Result(notSupport); + Server.BaseServer.Log(EventArgs.LogType.Warring, request.Session, $"HTTP {request.ID} {request.RemoteIPAddress} get { request.BaseUrl} file ext not support"); + //NotSupportResult notSupport = new NotSupportResult("get {0} file {1} ext not support", request.Url, request.Ext); + //response.Result(notSupport); + response.InnerError("403", $"file *.{request.Ext} not support"); } } @@ -525,8 +577,6 @@ private FileResource CreateResource(string file, bool cache) if (Debug) { fr = new NoCacheResource(file, urlname); - if (nogzip) - fr.GZIP = true; } else { @@ -541,6 +591,8 @@ private FileResource CreateResource(string file, bool cache) if (nogzip) fr.GZIP = true; } + if (info.Length < 1024 * 2) + fr.GZIP = false; } fr.Load(); diff --git a/src/Statistics.cs b/src/Statistics.cs new file mode 100644 index 0000000..6ab4fe4 --- /dev/null +++ b/src/Statistics.cs @@ -0,0 +1,486 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BeetleX.FastHttpApi +{ + public class Statistics + { + + const int COUNT = 701; + + public Statistics(string url) + { + + All = new CodeStatistics(0, "All"); + Url = url; + } + + private ConcurrentDictionary mCodeStatistics = new ConcurrentDictionary(); + + public CodeStatistics GetOrAddCodeStatistics(int code) + { + CodeStatistics result = new CodeStatistics(code); + if (!mCodeStatistics.TryAdd(code, result)) + { + mCodeStatistics.TryGetValue(code, out result); + } + return result; + } + public CodeStatistics GetCodeStatistics(int code) + { + mCodeStatistics.TryGetValue(code, out CodeStatistics result); + return result; + } + + public string Url { get; set; } + + public CodeStatistics OtherStatus { get; private set; } = new CodeStatistics(0, "Other"); + + public CodeStatistics Status_1xx { get; private set; } = new CodeStatistics(0, "1xx"); + + public CodeStatistics Status_2xx { get; private set; } = new CodeStatistics(0, "2xx"); + + public CodeStatistics Status_3xx { get; private set; } = new CodeStatistics(0, "3xx"); + + public CodeStatistics Status_4xx { get; private set; } = new CodeStatistics(0, "4xx"); + + public CodeStatistics Status_5xx { get; private set; } = new CodeStatistics(0, "5xx"); + + public CodeStatistics All { get; private set; } + + public int ItemID { get; set; } + + public void Add(int code, long time) + { + All.Add(time); + if (code >= 100 && code < 200) + Status_1xx.Add(time); + else if (code >= 200 && code < 300) + Status_2xx.Add(time); + else if (code >= 300 && code < 400) + Status_3xx.Add(time); + else if (code >= 400 && code < 500) + Status_4xx.Add(time); + else if (code >= 500 && code < 600) + Status_5xx.Add(time); + else + { + OtherStatus.Add(time); + } + if (code >= COUNT) + { + GetOrAddCodeStatistics(COUNT).Add(time); + } + else + { + GetOrAddCodeStatistics(code).Add(time); + } + } + + public StatisticsData ListStatisticsData(int code) + { + return GetOrAddCodeStatistics(code)?.GetData(); + } + public StatisticsData[] ListStatisticsData() + { + return (from a in this.mCodeStatistics.Values where a.Count > 0 orderby a.Count descending select a.GetData()).ToArray(); + } + public StatisticsData[] ListStatisticsData(params int[] codes) + { + List result = new List(); + foreach (var i in codes) + { + if (i < COUNT) + { + var stat = GetCodeStatistics(i); + if (stat != null) + result.Add(stat.GetData()); + } + } + return result.ToArray(); + } + + public StatisticsData[] ListStatisticsData(int start, int end) + { + List result = new List(); + for (int i = start; i < end; i++) + { + var stat = GetCodeStatistics(i); + if (stat != null) + result.Add(stat.GetData()); + } + return result.ToArray(); + } + + public object ListStatisticsData(int start, int end, Func selectObj) + { + var result = (from item in this.mCodeStatistics.Values + where item.Count > 0 && item.Code >= start && item.Code < end + select selectObj(item.GetData())).ToArray(); + return result; + } + + public CodeStatistics[] List(Func filters = null) + { + if (filters == null) + return (from a in this.mCodeStatistics.Values where a.Count > 0 orderby a.Count descending select a).ToArray(); + else + return (from a in this.mCodeStatistics.Values where a.Count > 0 && filters(a) orderby a.Count descending select a).ToArray(); + } + + public StatisticsGroup GetData() + { + StatisticsGroup result = new StatisticsGroup(this); + result.Url = Url; + result.Other = OtherStatus.GetData().Copy(); + result._1xx = Status_1xx.GetData().Copy(); + result._2xx = Status_2xx.GetData().Copy(); + result._3xx = Status_3xx.GetData().Copy(); + result._4xx = Status_4xx.GetData().Copy(); + result._5xx = Status_5xx.GetData().Copy(); + result.All = All.GetData().Copy(); + return result; + } + } + + public class CodeStatistics + { + public CodeStatistics(int code, string name = null) + { + Code = code; + if (name == null) + name = code.ToString(); + mLastTime = BeetleX.TimeWatch.GetTotalSeconds(); + Name = name; + mDelayStats.Add(new TimeDelayData(0, 0, 10, 0, 0)); + mDelayStats.Add(new TimeDelayData(1, 10, 20, 0, 0)); + mDelayStats.Add(new TimeDelayData(2, 20, 50, 0, 0)); + mDelayStats.Add(new TimeDelayData(3, 50, 100, 0, 0)); + mDelayStats.Add(new TimeDelayData(4, 100, 200, 0, 0)); + mDelayStats.Add(new TimeDelayData(5, 200, 500, 0, 0)); + mDelayStats.Add(new TimeDelayData(6, 500, 1000, 0, 0)); + mDelayStats.Add(new TimeDelayData(7, 1000, 2000, 0, 0)); + mDelayStats.Add(new TimeDelayData(8, 2000, 5000, 0, 0)); + mDelayStats.Add(new TimeDelayData(9, 5000, 10000, 0, 0)); + mDelayStats.Add(new TimeDelayData(10, 10000, 0, 0, 0)); + + } + + private List mDelayStats = new List(); + + public int Code { get; private set; } + + public string Name { get; set; } + + private long mCount; + + public long Count => mCount; + + private double mLastTime; + + private long mLastCount; + + public int Rps + { + get + { + double time = TimeWatch.GetTotalSeconds() - mLastTime; + int value = (int)((double)(mCount - mLastCount) / time); + mLastTime = TimeWatch.GetTotalSeconds(); + mLastCount = mCount; + return value; + } + } + + public void Add(long time) + { + System.Threading.Interlocked.Increment(ref mCount); + if (time <= 10) + System.Threading.Interlocked.Increment(ref ms10); + else if (time <= 20) + System.Threading.Interlocked.Increment(ref ms20); + else if (time <= 50) + System.Threading.Interlocked.Increment(ref ms50); + else if (time <= 100) + System.Threading.Interlocked.Increment(ref ms100); + else if (time <= 200) + System.Threading.Interlocked.Increment(ref ms200); + else if (time <= 500) + System.Threading.Interlocked.Increment(ref ms500); + else if (time <= 1000) + System.Threading.Interlocked.Increment(ref ms1000); + else if (time <= 2000) + System.Threading.Interlocked.Increment(ref ms2000); + else if (time <= 5000) + System.Threading.Interlocked.Increment(ref ms5000); + else if (time <= 10000) + System.Threading.Interlocked.Increment(ref ms10000); + else + System.Threading.Interlocked.Increment(ref msOther); + } + + public override string ToString() + { + return mCount.ToString(); + } + + private long ms10; + + private long ms10LastCount; + + public long Time10ms => ms10; + + private long ms20; + + private long ms20LastCount; + + public long Time20ms => ms20; + + private long ms50; + + private long ms50LastCount; + + public long Time50ms => ms50; + + private long ms100; + + private long ms100LastCount; + + public long Time100ms => ms100; + + private long ms200; + + private long ms200LastCount; + + public long Time200ms => ms200; + + private long ms500; + + private long ms500LastCount; + + public long Time500ms => ms500; + + private long ms1000; + + private long ms1000LastCount; + + public long Time1000ms => ms1000; + + private long ms2000; + + private long ms2000LastCount; + + public long Time2000ms => ms2000; + + private long ms5000; + + private long ms5000LastCount; + + public long Time5000ms => ms5000; + + private long ms10000; + + private long ms10000LastCount; + + public long Time10000ms => ms10000; + + private long msOther; + + private long msOtherLastCount; + + public long TimeOtherms => msOther; + + private double mLastRpsTime = 0; + + private int mGetStatus = 0; + + private int mGetDelayStatus = 0; + + public List GetDelay() + { + if (TimeWatch.GetTotalSeconds() - mLastRpsTime >= 1) + { + if (System.Threading.Interlocked.CompareExchange(ref mGetDelayStatus, 1, 0) == 0) + { + mDelayStats[0].Count = Time10ms; + mDelayStats[1].Count = Time20ms; + mDelayStats[2].Count = Time50ms; + mDelayStats[3].Count = Time100ms; + mDelayStats[4].Count = Time200ms; + mDelayStats[5].Count = Time500ms; + mDelayStats[6].Count = Time1000ms; + mDelayStats[7].Count = Time2000ms; + mDelayStats[8].Count = Time5000ms; + mDelayStats[9].Count = Time10000ms; + mDelayStats[10].Count = TimeOtherms; + double now = TimeWatch.GetTotalSeconds(); + double time = now - mLastRpsTime; + + mDelayStats[0].Rps = (int)((double)(ms10 - ms10LastCount) / time); + ms10LastCount = ms10; + + mDelayStats[1].Rps = (int)((double)(ms20 - ms20LastCount) / time); + ms20LastCount = ms20; + + mDelayStats[2].Rps = (int)((double)(ms50 - ms50LastCount) / time); + ms50LastCount = ms50; + + mDelayStats[3].Rps = (int)((double)(ms100 - ms100LastCount) / time); + ms100LastCount = ms100; + + mDelayStats[4].Rps = (int)((double)(ms200 - ms200LastCount) / time); + ms200LastCount = ms200; + + mDelayStats[5].Rps = (int)((double)(ms500 - ms500LastCount) / time); + ms500LastCount = ms500; + + mDelayStats[6].Rps = (int)((double)(ms1000 - ms1000LastCount) / time); + ms1000LastCount = ms1000; + + mDelayStats[7].Rps = (int)((double)(ms2000 - ms2000LastCount) / time); + ms2000LastCount = ms2000; + + mDelayStats[8].Rps = (int)((double)(ms5000 - ms5000LastCount) / time); + ms5000LastCount = ms5000; + + mDelayStats[9].Rps = (int)((double)(ms10000 - ms10000LastCount) / time); + ms10000LastCount = ms10000; + + mDelayStats[10].Rps = (int)((double)(msOther - msOtherLastCount) / time); + msOtherLastCount = msOther; + + mLastRpsTime = now; + mGetDelayStatus = 0; + } + } + return mDelayStats; + } + + private StatisticsData mStatisticsData = new StatisticsData(); + + public StatisticsData GetData() + { + if (TimeWatch.GetTotalSeconds() - mStatisticsData.CreateTime >= 1) + if (System.Threading.Interlocked.CompareExchange(ref mGetStatus, 1, 0) == 0) + { + StatisticsData result = mStatisticsData; + result.CreateTime = TimeWatch.GetTotalSeconds(); + result.Count = Count; + result.Rps = Rps; + result.Name = Name; + result.TimeDelays = GetDelay(); + mGetStatus = 0; + } + return mStatisticsData; + } + + } + + public class StatisticsGroup + { + public StatisticsGroup(Statistics statistics) + { + Statistics = statistics; + } + + public Statistics Statistics { get; set; } + + public String Url { get; set; } + + public string Server { get; set; } + + public StatisticsData All { get; set; } + + public StatisticsData Other { get; set; } + + public StatisticsData _1xx { get; set; } + public StatisticsData _2xx { get; set; } + public StatisticsData _3xx { get; set; } + public StatisticsData _4xx { get; set; } + public StatisticsData _5xx { get; set; } + + + } + + public class StatisticsData + { + public StatisticsData() + { + + } + + public string Name { get; set; } + + public long Count { get; set; } + + public long Rps { get; set; } + + public List TimeDelays { get; set; } = new List(); + + internal double CreateTime { get; set; } + + public StatisticsData Copy() + { + return new StatisticsData { Rps = Rps, Count = Count, Name = Name }; + } + + } + + public class TimeDelayData + { + public TimeDelayData(int index, int start, int end, long count, long rps) + { + StartTime = start; + EndTime = end; + Count = count; + Rps = rps; + Index = index; + } + + public int Index { get; set; } + + public int StartTime { get; set; } + + public int EndTime { get; set; } + + public long Count { get; set; } + + public long Rps { get; set; } + + public string Name + { + get + { + string name; + if (StartTime > 0 && EndTime > 0) + { + if (StartTime >= 1000) + name = $"{StartTime / 1000}s"; + else + name = $"{StartTime}ms"; + + if (EndTime >= 1000) + name += $"-{EndTime / 1000}s"; + else + name += $"-{EndTime}ms"; + + } + else if (StartTime > 0) + { + if (StartTime >= 1000) + name = $">{StartTime / 1000}s"; + else + name = $">{StartTime}ms"; + } + else + { + name = $"<{EndTime}ms"; + } + return name; + } + } + } +} diff --git a/src/Utils.cs b/src/Utils.cs index 74393f4..ce9518a 100644 --- a/src/Utils.cs +++ b/src/Utils.cs @@ -10,6 +10,8 @@ namespace BeetleX.FastHttpApi public class Utils { + public static IDGenerator IDGenerator { get; private set; } = new IDGenerator(); + public static string MD5Encrypt(Byte[] value) { using (MD5 md5Hash = MD5.Create()) diff --git a/src/VirtualFolder.cs b/src/VirtualFolder.cs index b90293f..329bf70 100644 --- a/src/VirtualFolder.cs +++ b/src/VirtualFolder.cs @@ -15,7 +15,7 @@ public void Verify() if (Folder[Folder.Length - 1] != '/') Folder += "/"; if (Folder[0] != '/') - Folder += "/"; + Folder = "/"+Folder; if (Path[Path.Length - 1] != System.IO.Path.DirectorySeparatorChar) { Path += System.IO.Path.DirectorySeparatorChar; diff --git a/src/WebSockets/DataFrame.cs b/src/WebSockets/DataFrame.cs index 005e6a1..d867f15 100644 --- a/src/WebSockets/DataFrame.cs +++ b/src/WebSockets/DataFrame.cs @@ -70,6 +70,8 @@ internal DataFrame(HttpApiServer server) private DataPacketLoadStep mLoadStep = DataPacketLoadStep.None; + internal HttpRequest Request { get; set; } + internal DataPacketLoadStep Read(PipeStream stream) { if (mLoadStep == DataPacketLoadStep.None) @@ -140,7 +142,7 @@ internal DataPacketLoadStep Read(PipeStream stream) { if (this.IsMask) ReadMask(stream); - Body = this.DataPacketSerializer.FrameDeserialize(this, stream); + Body = this.DataPacketSerializer.FrameDeserialize(this, stream, Request); mLoadStep = DataPacketLoadStep.Completed; } } @@ -176,6 +178,8 @@ private void ReadMask(PipeStream stream) } } + + private ulong MarkBytes(Span bytes, int start, int end, ulong index) { for (int i = start; i <= end; i++) @@ -203,7 +207,7 @@ void IDataResponse.Write(PipeStream stream) header[0] |= (byte)Type; if (Body != null) { - ArraySegment data = this.DataPacketSerializer.FrameSerialize(this, Body); + ArraySegment data = this.DataPacketSerializer.FrameSerialize(this, Body, Request); try { if (MaskKey == null || MaskKey.Length != 4) @@ -270,10 +274,6 @@ public void Send(ISession session, bool isError = false) mServer.IncrementResponsed(token.Request, null, 0, HttpApiServer.WEBSOCKET_SUCCESS, null); } session.Send(this); - if (session.Server.EnableLog(EventArgs.LogType.Info)) - { - session.Server.Log(EventArgs.LogType.Info, session, $"Websocket {token?.Request?.ID} {token?.Request?.RemoteIPAddress} websocket send data {this.Type.ToString()}"); - } } } } diff --git a/src/WebSockets/EventWebSocketReceive.cs b/src/WebSockets/EventWebSocketReceive.cs index e15e14a..42a28a2 100644 --- a/src/WebSockets/EventWebSocketReceive.cs +++ b/src/WebSockets/EventWebSocketReceive.cs @@ -1,5 +1,7 @@ -using System; +using BeetleX.Buffers; +using System; using System.Collections.Generic; +using System.IO; using System.Text; namespace BeetleX.FastHttpApi.WebSockets @@ -21,6 +23,20 @@ public void Response(DataFrame data) Sesson.Send(data); } + public void ResponseBinary(object data) + { + var frame = CreateFrame(data); + frame.Type = DataPacketType.binary; + Response(frame); + } + + public void ResponseText(object data) + { + var frame = CreateFrame(data); + frame.Type = DataPacketType.text; + Response(frame); + } + public DataFrame CreateFrame(object body = null) { var result = Server.CreateDataFrame(body); diff --git a/src/WebSockets/IDataPacketSerializer.cs b/src/WebSockets/IDataPacketSerializer.cs index 3c16ad9..f56a167 100644 --- a/src/WebSockets/IDataPacketSerializer.cs +++ b/src/WebSockets/IDataPacketSerializer.cs @@ -7,9 +7,9 @@ namespace BeetleX.FastHttpApi.WebSockets { public interface IDataFrameSerializer { - object FrameDeserialize(DataFrame data, PipeStream stream); + object FrameDeserialize(DataFrame data, PipeStream stream,HttpRequest request); - ArraySegment FrameSerialize(DataFrame packet, object body); + ArraySegment FrameSerialize(DataFrame packet, object body,HttpRequest request); void FrameRecovery(byte[] buffer);