星尘服务端,采用asp.net core 的 webapi
This commit is contained in:
parent
ecbff49b9c
commit
af6b815cb5
|
@ -0,0 +1,112 @@
|
|||
# EditorConfig is awesome:http://EditorConfig.org
|
||||
# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Don't use tabs for indentation.
|
||||
[*]
|
||||
indent_style = space
|
||||
# (Please don't specify an indent_size here; that has too many unintended consequences.)
|
||||
|
||||
# Code files
|
||||
[*.{cs,csx,vb,vbx}]
|
||||
indent_size = 4
|
||||
insert_final_newline = false
|
||||
charset = utf-8-bom
|
||||
end_of_line = crlf
|
||||
|
||||
# Xml project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml config files
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
# Dotnet code style settings:
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
#csharp_space_after_cast = true
|
||||
#csharp_space_after_keywords_in_control_flow_statements = true
|
||||
#csharp_space_between_method_declaration_parameter_list_parentheses = true
|
||||
#csharp_space_between_method_call_parameter_list_parentheses = true
|
||||
#csharp_space_between_parentheses = control_flow_statements, type_casts
|
||||
|
||||
# 单行放置代码
|
||||
csharp_preserve_single_line_statements = true
|
||||
csharp_preserve_single_line_blocks = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = false:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = false:suggestion
|
||||
#dotnet_style_require_accessibility_modifiers = for_non_interface_members:none/always:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
|
||||
# CSharp code style settings:
|
||||
[*.cs]
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:warning
|
||||
csharp_style_var_when_type_is_apparent = true:warning
|
||||
csharp_style_var_elsewhere = true:warning
|
||||
|
||||
# Prefer method-like constructs to have a block body
|
||||
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:suggestion
|
||||
csharp_style_expression_bodied_indexers = true:suggestion
|
||||
#csharp_style_expression_bodied_accessors = true:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# 单行不需要大括号
|
||||
csharp_prefer_braces = false:suggestion
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,54 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using NewLife.Log;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Stardust.Server.Common
|
||||
{
|
||||
/// <summary>统一Api过滤处理</summary>
|
||||
public sealed class ApiFilterAttribute : ActionFilterAttribute
|
||||
{
|
||||
/// <summary>执行前,验证模型</summary>
|
||||
/// <param name="context"></param>
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (!context.ModelState.IsValid)
|
||||
throw new ApplicationException(context.ModelState.Values.First(p => p.Errors.Count > 0).Errors[0].ErrorMessage);
|
||||
|
||||
base.OnActionExecuting(context);
|
||||
}
|
||||
|
||||
/// <summary>执行后,包装结果和异常</summary>
|
||||
/// <param name="context"></param>
|
||||
public override void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
if (context.Result != null)
|
||||
{
|
||||
if (context.Result is ObjectResult obj)
|
||||
{
|
||||
context.Result = new JsonResult(new { code = 0, data = obj.Value });
|
||||
}
|
||||
else if (context.Result is EmptyResult)
|
||||
{
|
||||
context.Result = new JsonResult(new { code = 0, data = new { } });
|
||||
}
|
||||
}
|
||||
else if (context.Exception != null && !context.ExceptionHandled)
|
||||
{
|
||||
var ex = context.Exception.GetTrue();
|
||||
if (ex is NewLife.Remoting.ApiException aex)
|
||||
context.Result = new JsonResult(new { code = aex.Code, data = aex.Message });
|
||||
else
|
||||
context.Result = new JsonResult(new { code = 500, data = ex.Message });
|
||||
|
||||
context.ExceptionHandled = true;
|
||||
|
||||
// 输出异常日志
|
||||
if (XTrace.Debug) XTrace.WriteException(ex);
|
||||
}
|
||||
|
||||
base.OnActionExecuted(context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Stardust.Server.Common
|
||||
{
|
||||
public class DateTimeConverter : JsonConverter<DateTime>
|
||||
{
|
||||
public String DateTimeFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => DateTime.Parse(reader.GetString());
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString(DateTimeFormat));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using NewLife.Remoting;
|
||||
using Stardust.Server.Controllers;
|
||||
|
||||
namespace Stardust.Server.Common
|
||||
{
|
||||
/// <summary>令牌校验</summary>
|
||||
public class TokenFilterAttribute : ActionFilterAttribute
|
||||
{
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.Controller is BaseController bc)
|
||||
{
|
||||
var session = bc.Session;
|
||||
if (bc.Token.IsNullOrEmpty()) throw new ApiException(403, "未授权");
|
||||
if (session == null) throw new ApiException(402, "令牌无效");
|
||||
}
|
||||
|
||||
base.OnActionExecuting(context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NewLife.Caching;
|
||||
using NewLife.Collections;
|
||||
|
||||
namespace Stardust.Server.Common
|
||||
{
|
||||
public class TokenSession
|
||||
{
|
||||
/// <summary>有效期</summary>
|
||||
public TimeSpan Expire { get; set; } = TimeSpan.FromMinutes(20);
|
||||
|
||||
/// <summary>会话存储</summary>
|
||||
public ICache Cache { get; set; } = new MemoryCache { Expire = 20 * 60, Period = 60 * 10 };
|
||||
|
||||
/// <summary>创建新的Session</summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public IDictionary<String, Object> CreateSession(String token)
|
||||
{
|
||||
var key = GetKey(token);
|
||||
var dic = Cache.GetDictionary<Object>(key);
|
||||
Cache.SetExpire(key, Expire);
|
||||
|
||||
//!! 临时修正可空字典的BUG
|
||||
if (Cache is MemoryCache mc)
|
||||
{
|
||||
dic = new NullableDictionary<String, Object>();
|
||||
mc.Set(key, dic);
|
||||
}
|
||||
|
||||
return dic;
|
||||
}
|
||||
|
||||
/// <summary>根据Token获取session</summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public IDictionary<String, Object> GetSession(String token)
|
||||
{
|
||||
if (token.IsNullOrEmpty()) return null;
|
||||
|
||||
var key = GetKey(token);
|
||||
// 当前缓存没有指定的token 则直接返回null
|
||||
if (!Cache.ContainsKey(key)) return null;
|
||||
|
||||
// 采用哈希结构。内存缓存用并行字段,Redis用Set
|
||||
var dic = Cache.GetDictionary<Object>(key);
|
||||
Cache.SetExpire(key, Expire);
|
||||
|
||||
return dic;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新token有效期
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="newToken"></param>
|
||||
public IDictionary<String, Object> CopySession(String token, String newToken)
|
||||
{
|
||||
if (newToken.IsNullOrEmpty()) return null;
|
||||
|
||||
var dic = GetSession(token);
|
||||
if (dic == null) return null;
|
||||
|
||||
var nkey = GetKey(newToken);
|
||||
if (Cache is MemoryCache mc)
|
||||
{
|
||||
mc.Set(nkey, dic, Expire);
|
||||
}
|
||||
//else if (Cache is Redis rds)
|
||||
//{
|
||||
// // redis.rename
|
||||
//}
|
||||
else
|
||||
{
|
||||
//var ndic = Cache.GetDictionary<Object>(nkey);
|
||||
var ndic = CreateSession(newToken);
|
||||
foreach (var item in dic)
|
||||
{
|
||||
ndic[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保建立新的
|
||||
return GetSession(newToken);
|
||||
}
|
||||
|
||||
///// <summary>修复session(服务器切换或机房切换情况)</summary>
|
||||
///// <returns></returns>
|
||||
//public IDictionary<String, Object> FixSession(String token)
|
||||
//{
|
||||
// if (token.IsNullOrEmpty()) return null;
|
||||
// var key = GetKey(token);
|
||||
|
||||
// // 采用哈希结构。内存缓存用并行字段,Redis用Set
|
||||
// var dic = Cache.GetDictionary<Object>(key);
|
||||
// Cache.SetExpire(key, Expire);
|
||||
|
||||
// return dic;
|
||||
//}
|
||||
|
||||
/// <summary>根据令牌活期缓存Key</summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual String GetKey(String token) => (!token.IsNullOrEmpty() && token.Length > 16) ? token.MD5() : token;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
|
||||
namespace Stardust.Server.Common
|
||||
{
|
||||
/// <summary>Web助手</summary>
|
||||
public static class WebHelper
|
||||
{
|
||||
/// <summary>获取用户主机</summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public static String GetUserHost(this HttpContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
|
||||
var str = "";
|
||||
if (str.IsNullOrEmpty()) str = request.Headers["HTTP_X_FORWARDED_FOR"];
|
||||
if (str.IsNullOrEmpty()) str = request.Headers["X-Real-IP"];
|
||||
if (str.IsNullOrEmpty()) str = request.Headers["X-Forwarded-For"];
|
||||
if (str.IsNullOrEmpty()) str = request.Headers["REMOTE_ADDR"];
|
||||
//if (str.IsNullOrEmpty()) str = request.Headers["Host"];
|
||||
//if (str.IsNullOrEmpty()) str = context.Connection?.RemoteIpAddress?.MapToIPv4() + "";
|
||||
if (str.IsNullOrEmpty())
|
||||
{
|
||||
var addr = context.Connection?.RemoteIpAddress;
|
||||
if (addr != null)
|
||||
{
|
||||
if (addr.IsIPv4MappedToIPv6) addr = addr.MapToIPv4();
|
||||
str = addr + "";
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace Stardust.Server.Common
|
||||
{
|
||||
static class XLoggerExtensions
|
||||
{
|
||||
public static ILoggingBuilder AddXLog(this ILoggingBuilder builder)
|
||||
{
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, XLoggerProvider>());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
[ProviderAlias("XLog")]
|
||||
class XLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(String categoryName)
|
||||
{
|
||||
var log = NewLife.Log.XTrace.Log;
|
||||
if (log is NewLife.Log.CompositeLog cp)
|
||||
{
|
||||
var tf = cp.Get<NewLife.Log.TextFileLog>();
|
||||
if (tf != null) log = tf;
|
||||
}
|
||||
|
||||
return new XLogger { Logger = log };
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
class XLogger : ILogger
|
||||
{
|
||||
public NewLife.Log.ILog Logger { get; set; }
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public Boolean IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return Logger.Enable;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, String> formatter)
|
||||
{
|
||||
if (!Logger.Enable) return;
|
||||
|
||||
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
|
||||
|
||||
var txt = formatter(state, exception);
|
||||
if (txt.IsNullOrEmpty() && exception == null) return;
|
||||
|
||||
switch (logLevel)
|
||||
{
|
||||
case LogLevel.Trace:
|
||||
case LogLevel.Debug:
|
||||
Logger.Debug(txt);
|
||||
break;
|
||||
case LogLevel.Information:
|
||||
Logger.Info(txt);
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
Logger.Warn(txt);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
Logger.Error(txt);
|
||||
break;
|
||||
case LogLevel.Critical:
|
||||
Logger.Fatal(txt);
|
||||
break;
|
||||
case LogLevel.None:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (exception != null) Logger.Error("{0}", exception);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using NewLife.Data;
|
||||
using NewLife.Reflection;
|
||||
using NewLife.Serialization;
|
||||
using Stardust.Server.Common;
|
||||
|
||||
namespace Stardust.Server.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class ApiController : ControllerBase
|
||||
{
|
||||
/// <summary>获取所有接口</summary>
|
||||
/// <returns></returns>
|
||||
[ApiFilter]
|
||||
[HttpGet]
|
||||
public Object Get() => Info(null);
|
||||
|
||||
private static readonly String _OS = Environment.OSVersion + "";
|
||||
private static readonly String _MachineName = Environment.MachineName;
|
||||
private static readonly String _UserName = Environment.UserName;
|
||||
private static readonly String _LocalIP = NetHelper.MyIP() + "";
|
||||
/// <summary>服务器信息,用户健康检测</summary>
|
||||
/// <param name="state">状态信息</param>
|
||||
/// <returns></returns>
|
||||
[ApiFilter]
|
||||
[HttpGet(nameof(Info))]
|
||||
public Object Info(String state)
|
||||
{
|
||||
var conn = HttpContext.Connection;
|
||||
var asmx = AssemblyX.Entry;
|
||||
var asmx2 = AssemblyX.Create(Assembly.GetExecutingAssembly());
|
||||
|
||||
var ip = HttpContext.GetUserHost();
|
||||
|
||||
var rs = new
|
||||
{
|
||||
Server = asmx?.Name,
|
||||
asmx?.FileVersion,
|
||||
asmx?.Compile,
|
||||
OS = _OS,
|
||||
MachineName = _MachineName,
|
||||
UserName = _UserName,
|
||||
ApiVersion = asmx2?.Version,
|
||||
|
||||
UserHost = HttpContext.GetUserHost(),
|
||||
LocalIP = _LocalIP,
|
||||
Remote = ip + "",
|
||||
State = state,
|
||||
//LastState = Session?["State"],
|
||||
Time = DateTime.Now,
|
||||
};
|
||||
|
||||
//// 记录上一次状态
|
||||
//if (Session != null) Session["State"] = state;
|
||||
|
||||
// 转字典
|
||||
var dic = rs.ToDictionary();
|
||||
|
||||
//// 令牌
|
||||
//if (!Token.IsNullOrEmpty()) dic["Token"] = Token;
|
||||
|
||||
// 时间和连接数
|
||||
//if (Host is ApiHost ah) dic["Uptime"] = (DateTime.Now - ah.StartTime).ToString();
|
||||
|
||||
dic["Port"] = conn.LocalPort;
|
||||
//dic["Online"] = nsvr.SessionCount;
|
||||
//dic["MaxOnline"] = nsvr.MaxSessionCount;
|
||||
|
||||
// 进程
|
||||
dic["Process"] = GetProcess();
|
||||
|
||||
//// 加上统计信息
|
||||
//dic["Stat"] = GetStat();
|
||||
|
||||
return dic;
|
||||
}
|
||||
|
||||
private Object GetProcess()
|
||||
{
|
||||
var proc = Process.GetCurrentProcess();
|
||||
|
||||
return new
|
||||
{
|
||||
Environment.ProcessorCount,
|
||||
ProcessId = proc.Id,
|
||||
Threads = proc.Threads.Count,
|
||||
Handles = proc.HandleCount,
|
||||
WorkingSet = proc.WorkingSet64,
|
||||
PrivateMemory = proc.PrivateMemorySize64,
|
||||
GCMemory = GC.GetTotalMemory(false),
|
||||
GC0 = GC.GetGeneration(0),
|
||||
GC1 = GC.GetGeneration(1),
|
||||
GC2 = GC.GetGeneration(2),
|
||||
};
|
||||
}
|
||||
|
||||
private static Packet _myInfo;
|
||||
/// <summary>服务器信息,用户健康检测,二进制压测</summary>
|
||||
/// <param name="state">状态信息</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost(nameof(Info2))]
|
||||
public async Task<ObjectResult> Info2()
|
||||
{
|
||||
if (_myInfo == null)
|
||||
{
|
||||
// 不包含时间和远程地址
|
||||
var rs = new
|
||||
{
|
||||
MachineNam = _MachineName,
|
||||
UserName = _UserName,
|
||||
LocalIP = _LocalIP,
|
||||
};
|
||||
_myInfo = new Packet(rs.ToJson().GetBytes());
|
||||
}
|
||||
|
||||
var buf = new Byte[4096];
|
||||
var count = await Request.Body.ReadAsync(buf, 0, buf.Length);
|
||||
var state = new Packet(buf, 0, count);
|
||||
|
||||
var pk = _myInfo.Slice(0, -1);
|
||||
pk.Append(state);
|
||||
|
||||
var res = new ObjectResult(pk.GetStream());
|
||||
res.Formatters.Add(new StreamOutputFormatter());
|
||||
res.ContentTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using NewLife.Log;
|
||||
using NewLife.Web;
|
||||
using Stardust.Data.Nodes;
|
||||
using Stardust.Server.Common;
|
||||
using XCode.Membership;
|
||||
|
||||
namespace Stardust.Server.Controllers
|
||||
{
|
||||
public abstract class BaseController : ControllerBase, IActionFilter
|
||||
{
|
||||
#region 属性
|
||||
/// <summary></summary>
|
||||
static readonly TokenSession _session = new TokenSession();
|
||||
|
||||
/// <summary>临时会话扩展信息</summary>
|
||||
public IDictionary<String, Object> Session { get; private set; }
|
||||
|
||||
/// <summary>令牌</summary>
|
||||
public String Token { get; private set; }
|
||||
|
||||
/// <summary>是否使用Cookie</summary>
|
||||
public Boolean UseCookies { get; set; }
|
||||
|
||||
/// <summary>用户主机</summary>
|
||||
public String UserHost => HttpContext.GetUserHost();
|
||||
#endregion
|
||||
|
||||
#region 校验
|
||||
/// <summary>请求处理后</summary>
|
||||
/// <param name="context"></param>
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
if (context.Exception != null)
|
||||
{
|
||||
// 拦截全局异常,写日志
|
||||
var action = context.HttpContext.Request.Path + "";
|
||||
if (context.ActionDescriptor is ControllerActionDescriptor act) action = $"{act.ControllerName}/{act.ActionName}";
|
||||
|
||||
var node = Session != null ? Session["Node"] as Node : null;
|
||||
WriteHistory(node, action, false, context.Exception?.GetTrue() + "");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>请求处理前</summary>
|
||||
/// <param name="context"></param>
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var ip = UserHost;
|
||||
ManageProvider.UserHost = ip;
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
var token = request.Query["Token"] + "";
|
||||
if (token.IsNullOrEmpty()) token = (request.Headers["Authorization"] + "").TrimStart("Bearer ");
|
||||
if (token.IsNullOrEmpty()) token = request.Headers["X-Token"] + "";
|
||||
if (token.IsNullOrEmpty()) token = request.Cookies["Token"] + "";
|
||||
|
||||
if (!token.IsNullOrEmpty())
|
||||
{
|
||||
Token = token;
|
||||
Session = _session.GetSession(token);
|
||||
// 考虑到可能出现的服务器切换或服务端闪断等情况
|
||||
if (Session == null) Session = RestoreSession(token);
|
||||
}
|
||||
else
|
||||
{
|
||||
CreateToken(null);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 令牌
|
||||
private static TokenProvider _tokenProvider;
|
||||
private static TokenProvider GetTokenProvider()
|
||||
{
|
||||
if (_tokenProvider != null) return _tokenProvider;
|
||||
|
||||
var provider = new TokenProvider();
|
||||
provider.ReadKey("../keys/token.prvkey", false);
|
||||
if (provider.Key.IsNullOrEmpty()) throw new ApplicationException("缺失私钥,无法创建令牌");
|
||||
|
||||
return _tokenProvider = provider;
|
||||
}
|
||||
|
||||
/// <summary>刷新令牌</summary>
|
||||
/// <param name="newTokedn"></param>
|
||||
protected void RefreshToken(String newToken)
|
||||
{
|
||||
Session = _session.CopySession(Token, newToken);
|
||||
Token = newToken;
|
||||
}
|
||||
|
||||
/// <summary>创建token同时刷新token有效期</summary>
|
||||
/// <param name="code"></param>
|
||||
protected void CreateToken(String code)
|
||||
{
|
||||
var set = Setting.Current;
|
||||
var expire = set.TokenExpire;
|
||||
|
||||
var provider = GetTokenProvider();
|
||||
|
||||
var token = provider.Encode(code, DateTime.Now.AddSeconds(expire));
|
||||
if (Token.IsNullOrEmpty())
|
||||
{
|
||||
// 创建新的token
|
||||
Session = _session.CreateSession(token);
|
||||
Token = token;
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshToken(token);
|
||||
}
|
||||
|
||||
if (UseCookies) Response.Cookies.Append("Token", Token);
|
||||
}
|
||||
|
||||
/// <summary>由token恢复session</summary>
|
||||
/// <param name="token"></param>
|
||||
private IDictionary<String, Object> RestoreSession(String token)
|
||||
{
|
||||
if (token.IsNullOrEmpty()) return null;
|
||||
|
||||
var str = token.Trim().Substring(null, ".")?.ToBase64().ToStr();
|
||||
var rlist = str?.Split('#', ',');
|
||||
if (rlist == null)
|
||||
{
|
||||
XTrace.WriteLine($"Token 解析失败!:{token}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var code = rlist[0];
|
||||
|
||||
var node = Node.FindByCode(code) ?? new Node { Code = code };
|
||||
|
||||
var provider = GetTokenProvider();
|
||||
|
||||
// token解码失败
|
||||
if (!provider.TryDecode(token, out var result, out var dt))
|
||||
{
|
||||
var msg = $"签名错误:{result ?? "null"} {dt:yyyy-MM-dd HH:mm:ss} {token}";
|
||||
XTrace.WriteLine(msg);
|
||||
WriteHistory(node, "签名错误", false, msg);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// token 过期
|
||||
if (dt < DateTime.Now)
|
||||
{
|
||||
var msg = $"令牌过期:{result ?? "null"} {dt:yyyy-MM-dd HH:mm:ss} {token}";
|
||||
XTrace.WriteLine(msg);
|
||||
WriteHistory(node, "令牌过期", false, msg);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
XTrace.WriteLine("借助令牌复活 {0}/{1}", code, token);
|
||||
|
||||
var dic = _session.CreateSession(token);
|
||||
if (dic == null) XTrace.WriteLine($"借助令牌复活 CreateSession null!");
|
||||
|
||||
dic["Node"] = node;
|
||||
|
||||
WriteHistory(node, "令牌复活", true, $"[{code}]{token}");
|
||||
|
||||
return dic;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 日志
|
||||
protected virtual void WriteHistory(INode node, String action, Boolean success, String remark)
|
||||
{
|
||||
NodeHistory.Create(node, action, success, remark, Environment.MachineName, UserHost);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -0,0 +1,512 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NewLife.Log;
|
||||
using NewLife.Remoting;
|
||||
using NewLife.Security;
|
||||
using NewLife.Serialization;
|
||||
using Stardust.Data.Nodes;
|
||||
using Stardust.Models;
|
||||
using Stardust.Server.Common;
|
||||
using XCode;
|
||||
|
||||
namespace Stardust.Server.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
[ApiFilter]
|
||||
public class NodeController : BaseController
|
||||
{
|
||||
#region 登录
|
||||
[HttpPost(nameof(Login))]
|
||||
public LoginResponse Login(LoginInfo inf)
|
||||
{
|
||||
var code = inf.Code;
|
||||
var secret = inf.Secret;
|
||||
|
||||
var node = Node.FindByCode(code, true);
|
||||
var di = inf.Node;
|
||||
|
||||
// 校验唯一编码,防止客户端拷贝配置
|
||||
if (node != null) node = CheckNode(node, di);
|
||||
|
||||
var autoReg = false;
|
||||
if (node == null)
|
||||
{
|
||||
node = AutoRegister(null, inf, out autoReg);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!node.Enable) throw new ApiException(99, "禁止登录");
|
||||
|
||||
// 登录密码未设置或者未提交,则执行动态注册
|
||||
if (node.Secret.IsNullOrEmpty() || secret.IsNullOrEmpty())
|
||||
node = AutoRegister(node, inf, out autoReg);
|
||||
else if (node.Secret.MD5() != secret)
|
||||
node = AutoRegister(node, inf, out autoReg);
|
||||
}
|
||||
|
||||
if (node == null) throw new ApiException(12, "节点鉴权失败");
|
||||
|
||||
var msg = "";
|
||||
var success = false;
|
||||
try
|
||||
{
|
||||
Fill(node, di);
|
||||
|
||||
//var ip = Request.Host + "";
|
||||
var ip = UserHost;
|
||||
|
||||
node.Logins++;
|
||||
node.LastLogin = DateTime.Now;
|
||||
node.LastLoginIP = ip;
|
||||
|
||||
if (node.CreateIP.IsNullOrEmpty()) node.CreateIP = ip;
|
||||
node.UpdateIP = ip;
|
||||
|
||||
node.Save();
|
||||
|
||||
// 设置令牌,可能已经进行用户登录
|
||||
CreateToken(node.Code);
|
||||
|
||||
Session["Node"] = node;
|
||||
|
||||
// 在线记录
|
||||
var olt = GetOnline(code, node);
|
||||
if (olt == null) olt = CreateOnline(code, node);
|
||||
|
||||
olt.LocalTime = di.Time;
|
||||
olt.MACs = di.Macs;
|
||||
olt.COMs = di.COMs;
|
||||
|
||||
olt.Token = Token;
|
||||
olt.PingCount++;
|
||||
|
||||
// 5秒内直接保存
|
||||
if (olt.CreateTime.AddSeconds(5) > DateTime.Now)
|
||||
olt.Save();
|
||||
else
|
||||
olt.SaveAsync();
|
||||
|
||||
msg = $"[{node.Name}/{node.Code}]鉴权成功 ";
|
||||
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
msg = ex.GetTrue().Message + " ";
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 登录历史
|
||||
WriteHistory("节点鉴权", success, node, msg + di.ToJson(false, false, false));
|
||||
|
||||
XTrace.WriteLine("登录{0} {1}", success ? "成功" : "失败", msg);
|
||||
}
|
||||
|
||||
var rs = new LoginResponse
|
||||
{
|
||||
Name = node.Name,
|
||||
Token = Token,
|
||||
};
|
||||
|
||||
// 动态注册,下发节点证书
|
||||
if (autoReg)
|
||||
{
|
||||
rs.Code = node.Code;
|
||||
rs.Secret = node.Secret;
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>注销</summary>
|
||||
/// <param name="reason">注销原因</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet(nameof(Logout))]
|
||||
[HttpPost(nameof(Logout))]
|
||||
public LoginResponse Logout(String reason)
|
||||
{
|
||||
var node = Session["Node"] as Node;
|
||||
if (node != null)
|
||||
{
|
||||
var olt = GetOnline(node.Code, node);
|
||||
if (olt != null)
|
||||
{
|
||||
var msg = "{3} [{0}]]登录于{1},最后活跃于{2}".F(node, olt.CreateTime, olt.UpdateTime, reason);
|
||||
WriteHistory(node, "节点下线", true, msg);
|
||||
olt.Delete();
|
||||
|
||||
// 计算在线时长
|
||||
if (olt.CreateTime.Year > 2000)
|
||||
{
|
||||
node.OnlineTime += (Int32)(DateTime.Now - olt.CreateTime).TotalSeconds;
|
||||
node.SaveAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁会话,更新令牌
|
||||
Session["Node"] = null;
|
||||
CreateToken(null);
|
||||
|
||||
return new LoginResponse
|
||||
{
|
||||
Name = node?.Name,
|
||||
Token = Token,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验节点密钥
|
||||
/// </summary>
|
||||
/// <param name="node"></param>
|
||||
/// <param name="ps"></param>
|
||||
/// <returns></returns>
|
||||
private Node CheckNode(Node node, NodeInfo di)
|
||||
{
|
||||
// 校验唯一编码,防止客户端拷贝配置
|
||||
var uuid = di.UUID;
|
||||
var guid = di.MachineGuid;
|
||||
if (!uuid.IsNullOrEmpty() && uuid != node.Uuid)
|
||||
{
|
||||
WriteHistory("登录校验", false, node, "唯一标识不符!{0}!={1}".F(uuid, node.Uuid));
|
||||
return null;
|
||||
}
|
||||
if (!guid.IsNullOrEmpty() && guid != node.MachineGuid)
|
||||
{
|
||||
WriteHistory("登录校验", false, node, "机器标识不符!{0}!={1}".F(guid, node.MachineGuid));
|
||||
return null;
|
||||
}
|
||||
|
||||
// 机器名
|
||||
if (di.MachineName != node.MachineName)
|
||||
{
|
||||
WriteHistory("登录校验", false, node, "机器名不符!{0}!={1}".F(di.MachineName, node.MachineName));
|
||||
}
|
||||
|
||||
// 网卡地址
|
||||
if (di.Macs != node.MACs)
|
||||
{
|
||||
var dims = di.Macs?.Split(",") ?? new String[0];
|
||||
var nodems = node.MACs?.Split(",") ?? new String[0];
|
||||
// 任意网卡匹配则通过
|
||||
if (!nodems.Any(e => dims.Contains(e)))
|
||||
{
|
||||
WriteHistory("登录校验", false, node, "网卡地址不符!{0}!={1}".F(di.Macs, node.MACs));
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private Node AutoRegister(Node node, LoginInfo inf, out Boolean autoReg)
|
||||
{
|
||||
var set = Setting.Current;
|
||||
if (!set.AutoRegister) throw new ApiException(12, "禁止自动注册");
|
||||
|
||||
var di = inf.Node;
|
||||
if (node == null)
|
||||
{
|
||||
// 该硬件的所有节点信息
|
||||
var list = Node.Search(di.UUID, di.MachineGuid, di.Macs);
|
||||
|
||||
// 当前节点信息,取较老者
|
||||
list = list.OrderBy(e => e.ID).ToList();
|
||||
|
||||
// 找到节点
|
||||
if (node == null) node = list.FirstOrDefault();
|
||||
}
|
||||
|
||||
var ip = UserHost;
|
||||
var name = "";
|
||||
if (name.IsNullOrEmpty()) name = di.MachineName;
|
||||
if (name.IsNullOrEmpty()) name = di.UserName;
|
||||
|
||||
if (node == null) node = new Node
|
||||
{
|
||||
Enable = true,
|
||||
|
||||
CreateIP = ip,
|
||||
CreateTime = DateTime.Now,
|
||||
};
|
||||
|
||||
// 如果未打开动态注册,则把节点修改为禁用
|
||||
node.Enable = set.AutoRegister;
|
||||
|
||||
if (node.Name.IsNullOrEmpty()) node.Name = name;
|
||||
|
||||
// 优先使用节点散列来生成节点证书,确保节点路由到其它接入网关时保持相同证书代码
|
||||
var code = "";
|
||||
var uid = $"{di.UUID}@{di.MachineGuid}@{di.Macs}";
|
||||
if (!uid.IsNullOrEmpty())
|
||||
{
|
||||
// 使用产品类别加密一下,确保不同类别有不同编码
|
||||
var buf = uid.GetBytes();
|
||||
code = buf.Crc().GetBytes().ToHex();
|
||||
|
||||
node.Code = code;
|
||||
}
|
||||
|
||||
if (node.Code.IsNullOrEmpty()) code = Rand.NextString(8);
|
||||
|
||||
node.Secret = Rand.NextString(16);
|
||||
node.UpdateIP = ip;
|
||||
node.UpdateTime = DateTime.Now;
|
||||
|
||||
node.Save();
|
||||
autoReg = true;
|
||||
|
||||
WriteHistory("动态注册", true, node, inf.ToJson(false, false, false));
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private void Fill(Node node, NodeInfo di)
|
||||
{
|
||||
if (!di.OSName.IsNullOrEmpty()) node.OS = di.OSName;
|
||||
if (!di.OSVersion.IsNullOrEmpty()) node.OSVersion = di.OSVersion;
|
||||
if (!di.Version.IsNullOrEmpty()) node.Version = di.Version;
|
||||
if (di.Compile.Year > 2000) node.CompileTime = di.Compile;
|
||||
|
||||
if (!di.MachineName.IsNullOrEmpty()) node.MachineName = di.MachineName;
|
||||
if (!di.UserName.IsNullOrEmpty()) node.UserName = di.UserName;
|
||||
if (!di.Processor.IsNullOrEmpty()) node.Processor = di.Processor;
|
||||
if (!di.CpuID.IsNullOrEmpty()) node.CpuID = di.CpuID;
|
||||
if (!di.UUID.IsNullOrEmpty()) node.Uuid = di.UUID;
|
||||
if (!di.MachineGuid.IsNullOrEmpty()) node.MachineGuid = di.MachineGuid;
|
||||
|
||||
if (di.ProcessorCount > 0) node.Cpu = di.ProcessorCount;
|
||||
if (di.Memory > 0) node.Memory = (Int32)(di.Memory / 1024 / 1024);
|
||||
if (!di.Macs.IsNullOrEmpty()) node.MACs = di.Macs;
|
||||
if (!di.COMs.IsNullOrEmpty()) node.COMs = di.COMs;
|
||||
if (!di.InstallPath.IsNullOrEmpty()) node.InstallPath = di.InstallPath;
|
||||
if (!di.Runtime.IsNullOrEmpty()) node.Runtime = di.Runtime;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 心跳
|
||||
[TokenFilter]
|
||||
//[HttpGet(nameof(Ping))]
|
||||
[HttpPost(nameof(Ping))]
|
||||
public PingResponse Ping(PingInfo inf)
|
||||
{
|
||||
var rs = new PingResponse
|
||||
{
|
||||
Time = inf.Time,
|
||||
ServerTime = DateTime.Now,
|
||||
};
|
||||
|
||||
var node = Session["Node"] as Node;
|
||||
if (node != null)
|
||||
{
|
||||
var code = node.Code;
|
||||
|
||||
var olt = GetOnline(code, node);
|
||||
if (olt == null) olt = CreateOnline(code, node);
|
||||
Fill(olt, inf);
|
||||
|
||||
olt.Token = Token;
|
||||
olt.PingCount++;
|
||||
olt.SaveAsync();
|
||||
|
||||
// 拉取命令
|
||||
var cmds = NodeCommand.AcquireCommands(node.ID, 100);
|
||||
if (cmds != null)
|
||||
{
|
||||
rs.Commands = cmds.Select(e => new CommandModel
|
||||
{
|
||||
Id = e.ID,
|
||||
Command = e.Command,
|
||||
Argument = e.Argument,
|
||||
}).ToArray();
|
||||
|
||||
foreach (var item in cmds)
|
||||
{
|
||||
item.Finished = true;
|
||||
item.UpdateTime = DateTime.Now;
|
||||
}
|
||||
cmds.Update(true);
|
||||
}
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
//[TokenFilter]
|
||||
[HttpGet(nameof(Ping))]
|
||||
public PingResponse Ping()
|
||||
{
|
||||
return new PingResponse
|
||||
{
|
||||
Time = 0,
|
||||
ServerTime = DateTime.Now,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>填充在线节点信息</summary>
|
||||
/// <param name="olt"></param>
|
||||
/// <param name="inf"></param>
|
||||
private void Fill(NodeOnline olt, PingInfo inf)
|
||||
{
|
||||
if (inf.AvailableMemory > 0) olt.AvailableMemory = (Int32)(inf.AvailableMemory / 1024 / 1024);
|
||||
if (inf.CpuRate > 0) olt.CpuRate = inf.CpuRate;
|
||||
if (inf.Delay > 0) olt.Delay = inf.Delay;
|
||||
|
||||
var dt = inf.Time.ToDateTime();
|
||||
if (dt.Year > 2000)
|
||||
{
|
||||
olt.LocalTime = dt;
|
||||
olt.Offset = (Int32)Math.Round((dt - DateTime.Now).TotalSeconds);
|
||||
}
|
||||
|
||||
if (!inf.Processes.IsNullOrEmpty()) olt.Processes = inf.Processes;
|
||||
if (!inf.Macs.IsNullOrEmpty()) olt.MACs = inf.Macs;
|
||||
if (!inf.COMs.IsNullOrEmpty()) olt.COMs = inf.COMs;
|
||||
}
|
||||
|
||||
/// <summary></summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="node"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual NodeOnline GetOnline(String code, INode node)
|
||||
{
|
||||
var olt = Session["Online"] as NodeOnline;
|
||||
if (olt != null) return olt;
|
||||
|
||||
var ip = UserHost;
|
||||
var port = HttpContext.Connection.RemotePort;
|
||||
var sid = $"{node.ID}@{ip}:{port}";
|
||||
return NodeOnline.FindBySessionID(sid);
|
||||
}
|
||||
|
||||
/// <summary>检查在线</summary>
|
||||
/// <returns></returns>
|
||||
protected virtual NodeOnline CreateOnline(String code, INode node)
|
||||
{
|
||||
var ip = UserHost;
|
||||
var port = HttpContext.Connection.RemotePort;
|
||||
var sid = $"{node.ID}@{ip}:{port}";
|
||||
var olt = NodeOnline.GetOrAdd(sid);
|
||||
olt.NodeID = node.ID;
|
||||
olt.Name = node.Name;
|
||||
|
||||
olt.Version = node.Version;
|
||||
olt.CompileTime = node.CompileTime;
|
||||
olt.Memory = node.Memory;
|
||||
olt.MACs = node.MACs;
|
||||
olt.COMs = node.COMs;
|
||||
olt.Token = Token;
|
||||
olt.CreateIP = ip;
|
||||
|
||||
olt.Creator = Environment.MachineName;
|
||||
|
||||
Session["Online"] = olt;
|
||||
|
||||
return olt;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 历史
|
||||
/// <summary>上报数据,针对命令</summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost(nameof(Report))]
|
||||
public async Task<Object> Report(Int32 id)
|
||||
{
|
||||
var node = Session["Node"] as Node;
|
||||
if (node == null) throw new ApiException(402, "节点未登录");
|
||||
|
||||
var cmd = NodeCommand.FindByID(id);
|
||||
if (cmd != null && cmd.NodeID == node.ID)
|
||||
{
|
||||
var ms = Request.Body;
|
||||
if (Request.ContentLength > 0)
|
||||
{
|
||||
var rs = "";
|
||||
switch (cmd.Command)
|
||||
{
|
||||
case "截屏":
|
||||
rs = await SaveFileAsync(cmd, ms, "png");
|
||||
break;
|
||||
case "抓日志":
|
||||
rs = await SaveFileAsync(cmd, ms, "log");
|
||||
break;
|
||||
default:
|
||||
rs = await SaveFileAsync(cmd, ms, "bin");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!rs.IsNullOrEmpty())
|
||||
{
|
||||
cmd.Result = rs;
|
||||
cmd.Save();
|
||||
|
||||
WriteHistory(node, cmd.Command, true, rs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<String> SaveFileAsync(NodeCommand cmd, Stream ms, String ext)
|
||||
{
|
||||
var file = $"../{cmd.Command}/{DateTime.Today:yyyyMMdd}/{cmd.NodeID}_{cmd.ID}.{ext}";
|
||||
file.EnsureDirectory(true);
|
||||
|
||||
using var fs = file.AsFile().OpenWrite();
|
||||
await ms.CopyToAsync(fs);
|
||||
await ms.FlushAsync();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
protected virtual NodeHistory WriteHistory(String action, Boolean success, INode node, String remark = null)
|
||||
{
|
||||
var ip = UserHost;
|
||||
return NodeHistory.Create(node, action, success, remark, Environment.MachineName, ip);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 升级
|
||||
/// <summary>升级检查</summary>
|
||||
/// <param name="channel">更新通道</param>
|
||||
/// <returns></returns>
|
||||
[TokenFilter]
|
||||
[HttpGet(nameof(Upgrade))]
|
||||
public UpgradeInfo Upgrade(String channel)
|
||||
{
|
||||
var node = Session["Node"] as Node;
|
||||
if (node == null) throw new ApiException(402, "节点未登录");
|
||||
|
||||
// 默认Release通道
|
||||
if (!Enum.TryParse<NodeChannels>(channel, true, out var ch)) ch = NodeChannels.Release;
|
||||
if (ch < NodeChannels.Release) ch = NodeChannels.Release;
|
||||
|
||||
// 找到所有产品版本
|
||||
var list = NodeUpgrade.GetValids(ch);
|
||||
|
||||
// 应用过滤规则
|
||||
var pv = list.FirstOrDefault(e => e.Match(node));
|
||||
if (pv == null) return null;
|
||||
//if (pv == null) throw new ApiException(509, "没有升级规则");
|
||||
|
||||
WriteHistory("自动更新", true, node, $"channel={ch} => [{pv.ID}] {pv.Version} {pv.Source} {pv.Executor}");
|
||||
|
||||
return new UpgradeInfo
|
||||
{
|
||||
Version = pv.Version,
|
||||
Source = pv.Source,
|
||||
Executor = pv.Executor,
|
||||
Force = pv.Force,
|
||||
Description = pv.Description,
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -1,132 +1,90 @@
|
|||
using NewLife;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NewLife;
|
||||
using NewLife.Agent;
|
||||
using NewLife.Log;
|
||||
using NewLife.Net;
|
||||
using NewLife.Remoting;
|
||||
using NewLife.Threading;
|
||||
using Stardust.Data;
|
||||
using System;
|
||||
|
||||
namespace Stardust.Server
|
||||
{
|
||||
class Program
|
||||
public class Program
|
||||
{
|
||||
static void Main(String[] args)
|
||||
public static void Main(String[] args)
|
||||
{
|
||||
new MyService().Main();
|
||||
if (Runtime.Windows)
|
||||
{
|
||||
var svc = new MyService
|
||||
{
|
||||
Args = args
|
||||
};
|
||||
svc.Main();
|
||||
}
|
||||
else
|
||||
{
|
||||
MyService.Run(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>服务类。名字可以自定义</summary>
|
||||
class MyService : AgentServiceBase<MyService>
|
||||
{
|
||||
/// <summary>是否使用线程池调度。false表示禁用线程池,改用Agent线程</summary>
|
||||
public Boolean Pooling { get; set; } = true;
|
||||
public String[] Args { get; set; }
|
||||
|
||||
public MyService()
|
||||
{
|
||||
ServiceName = "Stardust";
|
||||
ServiceName = "";
|
||||
|
||||
var set = XCode.Setting.Current;
|
||||
if (set.IsNew)
|
||||
{
|
||||
set.Debug = true;
|
||||
set.ShowSQL = false;
|
||||
set.TraceSQLTime = 3000;
|
||||
//set.SQLiteDbPath = @"..\Data";
|
||||
|
||||
set.Save();
|
||||
}
|
||||
var set2 = NewLife.Setting.Current;
|
||||
if (set2.IsNew)
|
||||
{
|
||||
set2.DataPath = @"..\Data";
|
||||
|
||||
set2.Save();
|
||||
}
|
||||
|
||||
ThreadPoolX.QueueUserWorkItem(() =>
|
||||
{
|
||||
var n = App.Meta.Count;
|
||||
AppStat.Meta.Session.Dal.Db.ShowSQL = false;
|
||||
});
|
||||
|
||||
// 注册菜单,在控制台菜单中按 t 可以执行Test函数,主要用于临时处理数据
|
||||
AddMenu('t', "数据测试", Test);
|
||||
// 异步初始化
|
||||
Task.Run(InitAsync);
|
||||
}
|
||||
|
||||
ApiServer _Server;
|
||||
private void Init()
|
||||
{
|
||||
var sc = _Server;
|
||||
if (sc == null)
|
||||
{
|
||||
var set = Setting.Current;
|
||||
|
||||
sc = new ApiServer(set.Port)
|
||||
{
|
||||
Log = XTrace.Log
|
||||
};
|
||||
if (set.Debug)
|
||||
{
|
||||
var ns = sc.EnsureCreate() as NetServer;
|
||||
ns.Log = XTrace.Log;
|
||||
#if DEBUG
|
||||
ns.LogSend = true;
|
||||
ns.LogReceive = true;
|
||||
sc.EncoderLog = XTrace.Log;
|
||||
#endif
|
||||
}
|
||||
|
||||
var local = new NetUri(NetType.Tcp, NetHelper.MyIP(), set.Port);
|
||||
|
||||
// 注册服务
|
||||
sc.Register<StarService>();
|
||||
|
||||
var ds = new DiscoverService
|
||||
{
|
||||
Name = Environment.MachineName,
|
||||
Local = local,
|
||||
};
|
||||
sc.Register(ds, null);
|
||||
|
||||
StarService.Log = XTrace.Log;
|
||||
StarService.Local = local;
|
||||
|
||||
sc.Start();
|
||||
|
||||
_Server = sc;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>服务启动</summary>
|
||||
/// <remarks>
|
||||
/// 安装Windows服务后,服务启动会执行一次该方法。
|
||||
/// 控制台菜单按5进入循环调试也会执行该方法。
|
||||
/// </remarks>
|
||||
private IWebHost _host;
|
||||
protected override void StartWork(String reason)
|
||||
{
|
||||
Init();
|
||||
_host = CreateWebHostBuilder(Args).Build();
|
||||
_host.RunAsync();
|
||||
|
||||
base.StartWork(reason);
|
||||
}
|
||||
|
||||
/// <summary>服务停止</summary>
|
||||
/// <remarks>
|
||||
/// 安装Windows服务后,服务停止会执行该方法。
|
||||
/// 控制台菜单按5进入循环调试,任意键结束时也会执行该方法。
|
||||
/// </remarks>
|
||||
protected override void StopWork(String reason)
|
||||
{
|
||||
base.StopWork(reason);
|
||||
_host.StopAsync().Wait(5_000);
|
||||
|
||||
_Server.TryDispose();
|
||||
_Server = null;
|
||||
base.StopWork(reason);
|
||||
}
|
||||
|
||||
/// <summary>数据测试,菜单t</summary>
|
||||
public void Test()
|
||||
public static void Run(String[] args)
|
||||
{
|
||||
XTrace.UseConsole();
|
||||
|
||||
// 异步初始化
|
||||
Task.Run(InitAsync);
|
||||
|
||||
CreateWebHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IWebHostBuilder CreateWebHostBuilder(String[] args)
|
||||
{
|
||||
var set = Setting.Current;
|
||||
|
||||
var builder = WebHost.CreateDefaultBuilder(args);
|
||||
|
||||
if (set.Port > 0) builder.UseUrls($"http://*:{set.Port}");
|
||||
|
||||
builder.UseStartup<Startup>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void InitAsync()
|
||||
{
|
||||
// 初始化数据库
|
||||
var n = App.Meta.Count;
|
||||
AppStat.Meta.Session.Dal.Db.ShowSQL = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:2006/",
|
||||
"sslPort": 44349
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Stardust.Server": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using NewLife.Xml;
|
||||
using NewLife.Configuration;
|
||||
|
||||
namespace Stardust.Server
|
||||
{
|
||||
/// <summary>配置</summary>
|
||||
[XmlConfigFile("Config/Stardust.config", 15000)]
|
||||
public class Setting : XmlConfig<Setting>
|
||||
[Config("Stardust")]
|
||||
public class Setting : Config<Setting>
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>调试开关。默认true</summary>
|
||||
|
@ -16,6 +16,14 @@ namespace Stardust.Server
|
|||
/// <summary>服务端口。默认6666</summary>
|
||||
[Description("服务端口。默认6666")]
|
||||
public Int32 Port { get; set; } = 6666;
|
||||
|
||||
/// <summary>令牌有效期。默认2*3600秒</summary>
|
||||
[Description("令牌有效期。默认2*3600秒")]
|
||||
public Int32 TokenExpire { get; set; } = 2 * 3600;
|
||||
|
||||
/// <summary>自动注册。允许客户端自动注册,默认true</summary>
|
||||
[Description("自动注册。允许客户端自动注册,默认true")]
|
||||
public Boolean AutoRegister { get; set; } = true;
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net45;netcoreapp2.1</TargetFrameworks>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<AssemblyTitle>星尘分布式服务</AssemblyTitle>
|
||||
<Description>星尘,分布式服务框架。分布式资源调度,服务自动注册和发现,负载均衡,动态伸缩,故障转移,性能监控。</Description>
|
||||
<Company>新生命开发团队</Company>
|
||||
<Copyright>版权所有(C) 新生命开发团队 2019</Copyright>
|
||||
<Version>1.0.2019.0526</Version>
|
||||
<FileVersion>1.0.2019.0526</FileVersion>
|
||||
<Copyright>©2002-2020 NewLife</Copyright>
|
||||
<Version>1.0.2020.0312</Version>
|
||||
<FileVersion>1.0.2020.0312</FileVersion>
|
||||
<AssemblyVersion>1.0.*</AssemblyVersion>
|
||||
<Deterministic>false</Deterministic>
|
||||
<OutputPath>..\BinServer</OutputPath>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)'=='netcoreapp2.1'">
|
||||
|
@ -28,12 +31,6 @@
|
|||
<Optimize>false</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net45'">
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NewLife.Agent" Version="8.6.2020.204" />
|
||||
<PackageReference Include="NewLife.XCode" Version="9.16.2020.308" />
|
||||
|
@ -44,12 +41,6 @@
|
|||
<ProjectReference Include="..\Stardust\Stardust.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
|
||||
<PackageReference Include="NewLife.Agent">
|
||||
<Version>8.1.2019.618</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="del "$(TargetDir)*.xml" /q" />
|
||||
</Target>
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Stardust.Server.Common;
|
||||
|
||||
namespace Stardust.Server
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration) => Configuration = configuration;
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHttpClient();
|
||||
|
||||
services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
|
||||
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
//app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
|
||||
namespace Stardust.Models
|
||||
{
|
||||
/// <summary>节点登录信息</summary>
|
||||
public class LoginInfo
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>节点编码</summary>
|
||||
public String Code { get; set; }
|
||||
|
||||
/// <summary>节点密钥</summary>
|
||||
public String Secret { get; set; }
|
||||
|
||||
/// <summary>节点信息</summary>
|
||||
public NodeInfo Node { get; set; }
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>节点登录响应</summary>
|
||||
public class LoginResponse
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>节点编码</summary>
|
||||
public String Code { get; set; }
|
||||
|
||||
/// <summary>节点密钥</summary>
|
||||
public String Secret { get; set; }
|
||||
|
||||
/// <summary>名称</summary>
|
||||
public String Name { get; set; }
|
||||
|
||||
/// <summary>令牌</summary>
|
||||
public String Token { get; set; }
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
|
||||
namespace Stardust.Models
|
||||
{
|
||||
/// <summary>节点信息</summary>
|
||||
public class NodeInfo
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>版本</summary>
|
||||
public String Version { get; set; }
|
||||
|
||||
/// <summary>编译时间</summary>
|
||||
public DateTime Compile { get; set; }
|
||||
|
||||
/// <summary>系统名</summary>
|
||||
public String OSName { get; set; }
|
||||
|
||||
/// <summary>系统版本</summary>
|
||||
public String OSVersion { get; set; }
|
||||
|
||||
/// <summary>机器名</summary>
|
||||
public String MachineName { get; set; }
|
||||
|
||||
/// <summary>用户名</summary>
|
||||
public String UserName { get; set; }
|
||||
|
||||
/// <summary>核心数</summary>
|
||||
public Int32 ProcessorCount { get; set; }
|
||||
|
||||
/// <summary>内存大小</summary>
|
||||
public UInt64 Memory { get; set; }
|
||||
|
||||
/// <summary>可用内存大小</summary>
|
||||
public UInt64 AvailableMemory { get; set; }
|
||||
|
||||
/// <summary>处理器</summary>
|
||||
public String Processor { get; set; }
|
||||
|
||||
/// <summary>处理器标识</summary>
|
||||
public String CpuID { get; set; }
|
||||
|
||||
/// <summary>主频</summary>
|
||||
public Single CpuRate { get; set; }
|
||||
|
||||
/// <summary>唯一标识</summary>
|
||||
public String UUID { get; set; }
|
||||
|
||||
/// <summary>机器标识</summary>
|
||||
public String MachineGuid { get; set; }
|
||||
|
||||
/// <summary>MAC地址</summary>
|
||||
public String Macs { get; set; }
|
||||
|
||||
/// <summary>串口</summary>
|
||||
public String COMs { get; set; }
|
||||
|
||||
/// <summary>安装路径</summary>
|
||||
public String InstallPath { get; set; }
|
||||
|
||||
/// <summary>运行时</summary>
|
||||
public String Runtime { get; set; }
|
||||
|
||||
/// <summary>本地时间</summary>
|
||||
public DateTime Time { get; set; }
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
|
||||
namespace Stardust.Models
|
||||
{
|
||||
/// <summary>心跳信息</summary>
|
||||
public class PingInfo
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>可用内存大小</summary>
|
||||
public UInt64 AvailableMemory { get; set; }
|
||||
|
||||
/// <summary>主频</summary>
|
||||
public Single CpuRate { get; set; }
|
||||
|
||||
/// <summary>MAC地址</summary>
|
||||
public String Macs { get; set; }
|
||||
|
||||
/// <summary>串口</summary>
|
||||
public String COMs { get; set; }
|
||||
|
||||
/// <summary>进程列表</summary>
|
||||
public String Processes { get; set; }
|
||||
|
||||
/// <summary>本地时间。ms毫秒</summary>
|
||||
public Int64 Time { get; set; }
|
||||
|
||||
/// <summary>延迟</summary>
|
||||
public Int32 Delay { get; set; }
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>心跳响应</summary>
|
||||
public class PingResponse
|
||||
{
|
||||
/// <summary>本地时间。ms毫秒</summary>
|
||||
public Int64 Time { get; set; }
|
||||
|
||||
/// <summary>服务器时间</summary>
|
||||
public DateTime ServerTime { get; set; }
|
||||
|
||||
/// <summary>下发命令</summary>
|
||||
public CommandModel[] Commands { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>命令模型</summary>
|
||||
public class CommandModel
|
||||
{
|
||||
/// <summary>序号</summary>
|
||||
public Int32 Id { get; set; }
|
||||
|
||||
/// <summary>命令</summary>
|
||||
public String Command { get; set; }
|
||||
|
||||
/// <summary>参数</summary>
|
||||
public String Argument { get; set; }
|
||||
|
||||
//public String Result { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
|
||||
namespace Stardust.Models
|
||||
{
|
||||
/// <summary>更新响应</summary>
|
||||
public class UpgradeInfo
|
||||
{
|
||||
/// <summary>版本号</summary>
|
||||
public String Version { get; set; }
|
||||
|
||||
/// <summary>更新源,Url地址</summary>
|
||||
public String Source { get; set; }
|
||||
|
||||
/// <summary>更新后要执行的命令</summary>
|
||||
public String Executor { get; set; }
|
||||
|
||||
/// <summary>是否强制更新,不需要用户同意</summary>
|
||||
public Boolean Force { get; set; }
|
||||
|
||||
/// <summary>描述</summary>
|
||||
public String Description { get; set; }
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<AssemblyVersion>1.0.*</AssemblyVersion>
|
||||
<Deterministic>false</Deterministic>
|
||||
</PropertyGroup>
|
||||
|
|
7
星尘.sln
7
星尘.sln
|
@ -5,7 +5,7 @@ VisualStudioVersion = 16.0.28922.388
|
|||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stardust", "Stardust\Stardust.csproj", "{AADBD913-749C-467E-A63F-C118C4C82351}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stardust.Web", "Stardust.Web\Stardust.Web.csproj", "{25331DEF-FEE3-44D5-A4E9-864078441F71}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stardust.Web", "Stardust.Web\Stardust.Web.csproj", "{25331DEF-FEE3-44D5-A4E9-864078441F71}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{0EA980BB-BB15-41A3-B75B-537BC42E985B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
|
@ -22,6 +22,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stardust.Server", "Stardust
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarAgent", "StarAgent\StarAgent.csproj", "{0FF65D90-214F-405E-9674-6C0992BC61FD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8D122272-87BB-4968-8A2F-CFF04CE47B29}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
Loading…
Reference in New Issue