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 _webServers = new(); /// /// 在新的 中开始 HTTP 服务端响应。 /// /// 服务端名称 /// 服务端实例 /// 是否成功开始,若已存在同名实例则返回 false 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; } /// /// 检查指定名称的 HTTP 服务端是否正在运行 /// /// 服务端名称 /// 是否正在运行 public static bool IsWebServerRunning(string name) { name = name.ToLowerInvariant(); return _webServers.ContainsKey(name); } /// /// 销毁 HTTP 服务端。若服务端正在运行,可能会引发异常。 /// /// 服务端名称 /// 是否成功销毁,若名称不存在或已经销毁则返回 false 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 _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 HandleCallback(HttpListenerRequest request) { if (!request.IsLocal) return HttpRouteResponse.Forbidden.AsTask(); var redirect = HttpRouteResponse.Redirect("/complete").AsTask(); // 解析回调 URL 参数 var parameterMap = new Dictionary(); 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 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(); pa["Port"] = Port.ToString(); _completeCallback(false, pa, _status.message); } } catch (Exception ex) { _status = OAuthCompleteStatus.Failed("处理回调出错", ex); } return HttpRouteResponse.Json(_status).AsTask(); } private Task 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 HandleIcon(HttpListenerRequest request) { return HttpRouteResponse.Input(ModBase.GetResourceStream("Images/icon.ico")).AsTask(); } private Task 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 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 }