初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
using PCL.Core.Link.Natayark;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class ModWebServer
|
||||
{
|
||||
private static readonly Dictionary<string, HttpServer> _webServers = new();
|
||||
|
||||
/// <summary>
|
||||
/// 在新的 <see cref="Task" /> 中开始 HTTP 服务端响应。
|
||||
/// </summary>
|
||||
/// <param name="name">服务端名称</param>
|
||||
/// <param name="server">服务端实例</param>
|
||||
/// <returns>是否成功开始,若已存在同名实例则返回 <c>false</c></returns>
|
||||
public static bool StartWebServer(string name, HttpServer server)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
lock (_webServers)
|
||||
{
|
||||
if (_webServers.ContainsKey(name))
|
||||
return false;
|
||||
_webServers[name] = server;
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
ModBase.Log($"[WebServer] 服务端 '{name}' 已启动");
|
||||
try
|
||||
{
|
||||
server.Start();
|
||||
// 保持服务器运行直到被停止(通过检查字典中是否还存在该服务器)
|
||||
while (true)
|
||||
{
|
||||
lock (_webServers)
|
||||
{
|
||||
if (!_webServers.ContainsKey(name))
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[WebServer] 服务端 '{name}' 运行出错");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
server.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略已释放的异常
|
||||
}
|
||||
|
||||
ModBase.Log($"[WebServer] 服务端 '{name}' 已停止");
|
||||
lock (_webServers)
|
||||
{
|
||||
_webServers.Remove(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定名称的 HTTP 服务端是否正在运行
|
||||
/// </summary>
|
||||
/// <param name="name">服务端名称</param>
|
||||
/// <returns>是否正在运行</returns>
|
||||
public static bool IsWebServerRunning(string name)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
return _webServers.ContainsKey(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销毁 HTTP 服务端。若服务端正在运行,可能会引发异常。
|
||||
/// </summary>
|
||||
/// <param name="name">服务端名称</param>
|
||||
/// <returns>是否成功销毁,若名称不存在或已经销毁则返回 <c>false</c></returns>
|
||||
public static bool DisposeWebServer(string name)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
lock (_webServers)
|
||||
{
|
||||
if (!_webServers.ContainsKey(name))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
_webServers[name].Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_webServers.Remove(name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#region 网页登录回调
|
||||
|
||||
public class NaidCallbackServer : HttpServer
|
||||
{
|
||||
private readonly OAuthComplete _completeCallback;
|
||||
private readonly string _picAddress;
|
||||
private readonly string _serviceName;
|
||||
private string _callbackContent;
|
||||
private IDictionary<string, string> _callbackParameters;
|
||||
|
||||
private OAuthCompleteStatus _status;
|
||||
|
||||
public NaidCallbackServer(string serviceName, OAuthComplete completeCallback, string picAddress) : base(new[]
|
||||
{ IPAddress.Parse("127.0.0.1") })
|
||||
{
|
||||
_serviceName = serviceName;
|
||||
_completeCallback = completeCallback;
|
||||
_picAddress = picAddress;
|
||||
}
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
// 注册回调路由
|
||||
Register(HttpMethod.Get, "/callback", HandleCallback);
|
||||
Register(HttpMethod.Post, "/callback", HandleCallback);
|
||||
|
||||
// 注册状态路由
|
||||
Register(HttpMethod.Get, "/status", HandleStatus);
|
||||
|
||||
// 注册资源路由
|
||||
Register(HttpMethod.Get, "/assets/background", HandleBackground);
|
||||
Register(HttpMethod.Get, "/assets/icon", HandleIcon);
|
||||
Register(HttpMethod.Get, "/complete", HandleComplete);
|
||||
}
|
||||
|
||||
private Task<HttpRouteResponse> HandleCallback(HttpListenerRequest request)
|
||||
{
|
||||
if (!request.IsLocal)
|
||||
return HttpRouteResponse.Forbidden.AsTask();
|
||||
|
||||
var redirect = HttpRouteResponse.Redirect("/complete").AsTask();
|
||||
|
||||
// 解析回调 URL 参数
|
||||
var parameterMap = new Dictionary<string, string>();
|
||||
var query = request.Url.Query;
|
||||
var queryIndex = query.IndexOf('?');
|
||||
if (queryIndex != -1 && query.Length > queryIndex)
|
||||
try
|
||||
{
|
||||
var sq = query.Substring(queryIndex + 1).Split('&');
|
||||
var splitChar = new[] { '=' };
|
||||
foreach (var iq in sq)
|
||||
{
|
||||
var q = iq.Split(splitChar, 2);
|
||||
if (q.Length == 2) parameterMap[q[0]] = q[1];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_status = OAuthCompleteStatus.Failed("回调参数解析出错", ex);
|
||||
return redirect;
|
||||
}
|
||||
|
||||
_callbackParameters = parameterMap;
|
||||
|
||||
// 读取回调内容
|
||||
if (request.HasEntityBody)
|
||||
try
|
||||
{
|
||||
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
|
||||
{
|
||||
_callbackContent = reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_status = OAuthCompleteStatus.Failed("读取回调内容出错", ex);
|
||||
return redirect;
|
||||
}
|
||||
|
||||
return redirect;
|
||||
}
|
||||
|
||||
private Task<HttpRouteResponse> HandleStatus(HttpListenerRequest request)
|
||||
{
|
||||
if (_callbackParameters is null)
|
||||
return HttpRouteResponse.NotFound.AsTask();
|
||||
|
||||
try
|
||||
{
|
||||
if (_status is null)
|
||||
{
|
||||
_callbackParameters["Port"] = Port.ToString();
|
||||
_status = _completeCallback(true, _callbackParameters, _callbackContent);
|
||||
}
|
||||
else if (!_status.success)
|
||||
{
|
||||
ModBase.Log($"[OAuth] {_serviceName}: {_status.message}{"\r\n"}{_status.stacktrace}");
|
||||
var pa = new Dictionary<string, string>();
|
||||
pa["Port"] = Port.ToString();
|
||||
_completeCallback(false, pa, _status.message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_status = OAuthCompleteStatus.Failed("处理回调出错", ex);
|
||||
}
|
||||
|
||||
return HttpRouteResponse.Json(_status).AsTask();
|
||||
}
|
||||
|
||||
private Task<HttpRouteResponse> HandleBackground(HttpListenerRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_picAddress))
|
||||
return Task.FromResult(HttpRouteResponse.NotFound);
|
||||
return HttpRouteResponse
|
||||
.Input(new FileStream(_picAddress, FileMode.Open, FileAccess.Read, FileShare.None, 16384, true))
|
||||
.AsTask();
|
||||
}
|
||||
|
||||
private Task<HttpRouteResponse> HandleIcon(HttpListenerRequest request)
|
||||
{
|
||||
return HttpRouteResponse.Input(ModBase.GetResourceStream("Images/icon.ico")).AsTask();
|
||||
}
|
||||
|
||||
private Task<HttpRouteResponse> HandleComplete(HttpListenerRequest request)
|
||||
{
|
||||
return HttpRouteResponse.Input(ModBase.GetResourceStream("Resources/oauth-complete.html"), "text/html")
|
||||
.AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object changeLock = new();
|
||||
private static string picAddress;
|
||||
|
||||
public static object BackgroundPicChangeCallback(string pic)
|
||||
{
|
||||
lock (changeLock)
|
||||
{
|
||||
picAddress = pic;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class OAuthCompleteStatus
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string username { get; set; }
|
||||
public string message { get; set; }
|
||||
public string stacktrace { get; set; }
|
||||
|
||||
public static OAuthCompleteStatus Complete(string username)
|
||||
{
|
||||
return new OAuthCompleteStatus { success = true, username = username };
|
||||
}
|
||||
|
||||
public static OAuthCompleteStatus Failed(string message, Exception ex = null)
|
||||
{
|
||||
var init = new OAuthCompleteStatus();
|
||||
return (init.success = false, init.message = message, init.stacktrace = ex?.ToString(), init).init;
|
||||
}
|
||||
}
|
||||
|
||||
public delegate OAuthCompleteStatus? OAuthComplete(bool success, IDictionary<string, string> parameters,
|
||||
string content);
|
||||
|
||||
public static bool StartOAuthWaitingCallback(string serviceName, string url, OAuthComplete completeCallback)
|
||||
{
|
||||
if (IsWebServerRunning(serviceName))
|
||||
return false;
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
var serverPort = 0;
|
||||
lock (_webServers)
|
||||
{
|
||||
NaidCallbackServer? server;
|
||||
|
||||
string currentPicAddress;
|
||||
lock (changeLock)
|
||||
{
|
||||
currentPicAddress = picAddress;
|
||||
}
|
||||
|
||||
server = new NaidCallbackServer(serviceName, completeCallback, currentPicAddress);
|
||||
serverPort = server.Port;
|
||||
ModBase.Log($"[OAuth] {serviceName}: 已开始监听 {server.Port} 端口,正在初始化路由");
|
||||
// 开始响应请求
|
||||
var webServiceName = $"oauth/{serviceName}";
|
||||
if (DisposeWebServer(webServiceName)) ModBase.Log("[OAuth] 已关闭先前认证服务服务端");
|
||||
StartWebServer(webServiceName, server);
|
||||
ModBase.Log($"[OAuth] {serviceName}: 初始化完成,开始响应 HTTP 请求");
|
||||
}
|
||||
|
||||
// 打开 OAuth URL
|
||||
ModBase.OpenWebsite(url.Replace("%r", $"http://localhost:{serverPort}/callback"));
|
||||
}, $"CallbackWebServerLoading/{serviceName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void StartNaidAuthorize(Action? completeCallback = null)
|
||||
{
|
||||
Exception? resultEx = null;
|
||||
StartOAuthWaitingCallback("NatayarkID",
|
||||
$"https://account.naids.com/oauth2/authorize?response_type=code&client_id={Secrets.NatayarkClientId}&redirect_uri=%r",
|
||||
(success, parameters, content) =>
|
||||
{
|
||||
OAuthCompleteStatus? status;
|
||||
if (!success)
|
||||
{
|
||||
ModMain.MyMsgBox(content, isWarn: true);
|
||||
completeCallback?.Invoke();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
var code = parameters["code"];
|
||||
|
||||
try
|
||||
{
|
||||
NatayarkProfileManager.GetNaidDataAsync(code, port: ushort.Parse(parameters["Port"])).Wait();
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
resultEx = ex.InnerExceptions[0];
|
||||
}
|
||||
|
||||
if (resultEx is null)
|
||||
status = OAuthCompleteStatus.Complete(NatayarkProfileManager.NaidProfile.Username);
|
||||
else
|
||||
status = OAuthCompleteStatus.Failed("获取用户信息失败,请尝试重新登录", resultEx);
|
||||
completeCallback?.Invoke();
|
||||
return status;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user