#if NETFX || NETCORE using Apewer; using Apewer.Models; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Reflection; #if NETFX using System.Web; #endif #if NETCORE using Microsoft.AspNetCore.Http; #endif namespace Apewer.Web { internal static class ApiInternals { #region Fields private static Dictionary _entries = null; private static List _assemblies = null; #endregion #region Relection public static Dictionary Entries { get { return _entries; } set { _entries = value; } } public static List Assemblies { get { return _assemblies; } set { _assemblies = value; } } internal static Dictionary GetEntries(IEnumerable assemblies, bool sort = true) { var entries = new Dictionary(); var deduplicates = new List(); foreach (var assembly in assemblies) { if (assembly == null) continue; if (deduplicates.Contains(assembly)) continue; deduplicates.Add(assembly); } _assemblies = deduplicates; var aes = new ObjectSet(); foreach (var assembly in deduplicates) { var types = ClassUtility.GetTypes(assembly); foreach (var type in types) { var application = GetApplication(type); if (application == null) continue; aes[application.Name] = application; var methods = type.GetMethods(); var fes = new ObjectSet(true); foreach (var method in methods) { var function = GetFunction(application, method); if (function == null) continue; fes[function.Name] = function; } var fns = new List(); fns.AddRange(ClassUtility.GetOrigin(fes).Keys); if (sort) fns.Sort(); foreach (var i in fns) application.Functions.Add(i, fes[i]); } } var ans = new List(); ans.AddRange(ClassUtility.GetOrigin(aes).Keys); if (sort) ans.Sort(); foreach (var i in ans) entries.Add(i, aes[i]); return entries; } internal static ApiApplication GetApplication(Type type) { if (type == null) return null; // Check Type Properties if (!type.IsClass) return null; if (type.IsAbstract) return null; if (type.IsGenericType) return null; // Check Type ApiAttributes var attributes = type.GetCustomAttributes(typeof(ApiAttribute), false); if (attributes.Length < 1) return null; // Check Base if (!ClassUtility.IsInherits(type, typeof(ApiController))) return null; // Entry var entry = new ApiApplication(); entry.Type = type; entry.Assembly = type.Assembly; // Name var api = (ApiAttribute)attributes[0]; var name = api.Name; if (string.IsNullOrEmpty(name)) name = type.Name; name = name.ToLower(); entry.Name = name; // Caption entry.Caption = api.Caption; if (entry.Caption.Length < 1) { var captions = type.GetCustomAttributes(typeof(CaptionAttribute), true); if (captions.Length > 0) { var caption = (CaptionAttribute)captions[0]; entry.Caption = caption.Title; entry.Description = caption.Description; } } // Visible entry.Visible = api.Visible; if (entry.Visible) { if (type.ContainsAttribute(false)) { entry.Visible = false; } } // Independent if (type.ContainsAttribute(false)) { entry.Independent = true; } // Module entry.Module = TextUtility.Join("-", entry.Assembly.GetName().Name, entry.Assembly.GetName().Version.ToString()); return entry; } internal static ApiFunction GetFunction(ApiApplication application, MethodInfo method) { if (method == null) return null; // 滤除构造函数。 if (method.IsConstructor) return null; // 滤除继承的方法。 if (method.DeclaringType.Equals(typeof(object))) return null; if (method.DeclaringType.Equals(typeof(ApiController))) return null; // 滤除带参数的方法。 var parameters = method.GetParameters(); if (parameters.Length > 0) return null; // Entry var entry = new ApiFunction(); entry.Type = application.Type; entry.Assembly = application.Type.Assembly; entry.Method = method; entry.Name = method.Name.ToLower(); // Caption var captions = method.GetCustomAttributes(typeof(CaptionAttribute), true); if (captions.Length > 0) { var caption = (CaptionAttribute)captions[0]; entry.Caption = caption.Title; entry.Description = caption.Description; } // Visible entry.Visible = true; if (application.Visible) { var hidden = method.GetCustomAttributes(typeof(HiddenAttribute), false); if (hidden.Length > 0) entry.Visible = false; } else { entry.Visible = false; } // Returnable if (method.ReturnType.Equals(typeof(string))) { entry.Returnable = true; } return entry; } #endregion #region ApiRequest internal static StringPairs ParseUrlParameters #if NETFX (HttpRequest request) => WebUtility.ParseParameters(request.Url.Query); #else (HttpRequest request) { var list = new StringPairs(); foreach (var key in request.Query.Keys) { if (request != null) { list.Add(new KeyValuePair(key, request.Query[key])); } } return list; } #endif internal static ApiRequest GetRequest(HttpRequest request) { if (request == null) return null; if (string.IsNullOrEmpty(request.Path)) return null; var apiRequest = new ApiRequest(); apiRequest.IP = WebUtility.GetClientIP(request, true); apiRequest.Url = WebUtility.GetUrl(request); apiRequest.Parameters = ParseUrlParameters(request); apiRequest.UserAgent = WebUtility.GetUserAgent(request); #if NETFX apiRequest.Referrer = request.UrlReferrer; #endif // 获取 Http Method。 apiRequest.Method = WebUtility.GetMethod(request); // 准备变量。 var application = null as string; var function = null as string; var random = null as string; var ticket = null as string; var session = null as string; var page = null as string; // 头。 foreach (var kvp in WebUtility.GetHeaders(request)) { apiRequest.Headers.Add(new KeyValuePair(kvp.Key, kvp.Value)); switch (TextUtility.ToLower(kvp.Key)) { case "user-agent": apiRequest.UserAgent = kvp.Value; break; } } // Cookies。 apiRequest.Cookies = WebUtility.GetCookies(request); // 解析 POST 请求。 if (apiRequest.Method == Apewer.Network.HttpMethod.POST) { #if NETFX var post = BinaryUtility.Read(request.InputStream); #else var post = BinaryUtility.Read(request.Body); #endif var text = TextUtility.FromBinary(post); var json = Json.Parse(text) ?? Json.NewObject(); application = json["application"]; function = json["function"]; random = json["random"]; ticket = json["ticket"]; session = json["session"]; page = json["page"]; var data = json.GetProperty("data"); apiRequest.PostData = post; apiRequest.PostText = text; apiRequest.PostJson = json; apiRequest.Data = data ?? Json.NewObject(); } // 解析 URL 参数。 // URL 参数的优先级应高于 URL 路径,以避免反向代理产生的路径问题。 if (string.IsNullOrEmpty(application)) application = WebUtility.GetParameter(apiRequest.Parameters, "application"); if (string.IsNullOrEmpty(function)) function = WebUtility.GetParameter(apiRequest.Parameters, "function"); if (string.IsNullOrEmpty(random)) random = WebUtility.GetParameter(apiRequest.Parameters, "random"); if (string.IsNullOrEmpty(ticket)) ticket = WebUtility.GetParameter(apiRequest.Parameters, "ticket"); if (string.IsNullOrEmpty(session)) session = WebUtility.GetParameter(apiRequest.Parameters, "session"); if (string.IsNullOrEmpty(page)) page = WebUtility.GetParameter(apiRequest.Parameters, "page"); // 从 Cookie 中获取 Ticket。 if (string.IsNullOrEmpty(ticket)) ticket = apiRequest.Cookies.GetValue("ticket"); // 最后检查 URL 路径。 var paths = request.Path.ToString().Split('/'); if (string.IsNullOrEmpty(application) && paths.Length >= 2) application = TextUtility.DecodeUrl(paths[1]); if (string.IsNullOrEmpty(function) && paths.Length >= 3) function = TextUtility.DecodeUrl(paths[2]); // 修正内容。 application = application.SafeLower().SafeTrim(); function = function.SafeLower().SafeTrim(); random = random.SafeLower().SafeTrim(); ticket = ticket.SafeLower().SafeTrim(); session = session.SafeLower().SafeTrim(); page = page.SafeTrim(); // 设置请求:回传。 apiRequest.Application = application; apiRequest.Function = function; apiRequest.Random = random; // 设置请求:不回传。 apiRequest.Ticket = ticket; apiRequest.Session = session; apiRequest.Page = page; // 返回结果。 return apiRequest; } #endregion #region ApiResponse /// 设置 status 为 error,并设置 message 的内容。 internal static void RespondError(ApiResponse response, string message = "未知错误。") { if (response == null) return; response.Type = ApiFormat.Json; response.Status = "error"; response.Message = message ?? TextUtility.EmptyString; } /// 设置 status 为 error,并设置 message 的内容。 internal static void RespondError(ApiResponse response, Exception exception) { if (response == null) return; response.Exception = exception; response.Type = ApiFormat.Json; response.Status = "error"; try { response.Message = exception == null ? "无效异常。" : exception.Message; response.Data.Reset(Json.NewObject()); response.Data["message"] = exception.Message; response.Data["helplink"] = exception.HelpLink; response.Data["source"] = exception.Source; response.Data["stacktrace"] = Json.Parse(exception.StackTrace.Split('\n'), true); } catch { } } /// 输出 UTF-8 文本。 internal static void RespondText(ApiResponse response, string content, string type = "text/plain; charset=utf-8") { if (response == null) return; response.Type = ApiFormat.Text; response.TextString = content; response.TextType = type ?? "text/plain; charset=utf-8"; } /// 输出字节数组。 internal static void RespondBinary(ApiResponse response, byte[] content, string type = "application/octet-stream") { if (response == null) return; response.Type = ApiFormat.Binary; response.BinaryStream = null; response.BinaryBytes = content; response.BinaryType = type ?? "application/octet-stream"; } /// 输出二进制。 internal static void RespondBinary(ApiResponse response, Stream content, string type = "application/octet-stream") { if (response == null) return; response.Type = ApiFormat.Binary; response.BinaryStream = content; response.BinaryBytes = null; response.BinaryType = type ?? "application/octet-stream"; } /// 输出文件。 internal static void RespondFile(ApiResponse response, Stream stream, string name, string type = "application/octet-stream") { if (response == null) return; response.Type = ApiFormat.File; response.FileStream = stream; response.FileName = name; response.FileType = type ?? "application/octet-stream"; } /// 重定向。 public static void RespondRedirect(ApiResponse response, string url) { if (response == null) return; response.Type = ApiFormat.Redirect; response.RedirectUrl = url; } internal static string ExportJson(ApiResponse response, bool indented = true, bool exception = false) { if (response == null) return "{}"; var json = Json.NewObject(); json.SetProperty("beginning", response.Beginning ?? TextUtility.EmptyString); json.SetProperty("ending", response.Ending ?? TextUtility.EmptyString); json.SetProperty("random", response.Random); json.SetProperty("application", response.Application); json.SetProperty("function", response.Function); json.SetProperty("status", (TextUtility.IsBlank(response.Status) ? TextUtility.EmptyString : response.Status.ToLower())); json.SetProperty("message", response.Message); if (exception) { if (response.Exception == null) json.SetProperty("exception"); else { var exmessage = null as string; var exstacktrace = null as string; var exsource = null as string; var exhelplink = null as string; try { exmessage = response.Exception.Message; exstacktrace = response.Exception.StackTrace; exsource = response.Exception.Source; exhelplink = response.Exception.HelpLink; } catch { } var exjson = Json.NewObject(); exjson.SetProperty("type", response.Exception.GetType().FullName); exjson.SetProperty("message", exmessage); exjson.SetProperty("stack", Json.Parse((exstacktrace ?? "").Replace("\r", "").Split('\n'), false)); exjson.SetProperty("source", exsource); exjson.SetProperty("helplink", exhelplink); if (response.Exception is System.Net.WebException) { var webex = response.Exception as System.Net.WebException; { var array = Json.NewArray(); foreach (var k in webex.Data.Keys) { var item = Json.NewObject(); var v = webex.Data[k]; item.SetProperty(k.ToString(), Json.Parse(v.ToString())); } exjson.SetProperty("data", array); } exjson.SetProperty("", Json.Parse(webex.Response, true)); } json.SetProperty("exception", exjson); } } json.SetProperty("data", response.Data); var text = json.ToString(indented); return text; } #endregion #region HttpResponse public static void AddHeader(HttpResponse response, string name, string value) { if (response == null || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) return; #if NET20 try { response.AddHeader(name, value); } catch { } #else try { response.Headers.Add(name, value); } catch { } #endif } public static void AddHeaders(HttpResponse response, NameValueCollection collection) { if (response == null || collection == null) return; foreach (var key in collection.AllKeys) AddHeader(response, key, collection[key]); } public static void AddHeaders(HttpResponse response, StringPairs headers) { if (response == null || headers == null) return; foreach (var key in headers.GetAllKeys()) { var values = headers.GetValues(key); foreach (var value in values) AddHeader(response, key, value); } } #if NETFX private static FieldInfo CacheControlField = null; private static void SetCacheControlField(HttpResponse response, string value) { if (CacheControlField == null) { var type = typeof(HttpResponse); var field = type.GetField("_cacheControl", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); if (field == null) return; CacheControlField = field; } if (CacheControlField != null) CacheControlField.SetValue(response, value); } #endif /// 设置缓存时间,单位为秒,最大为 2592000 秒(30 天)。 public static void SetCacheControl (HttpResponse response, int seconds = 0) { if (response == null) return; var s = seconds; if (s < 0) s = 0; if (s > 2592000) s = 2592000; #if NETFX if (s > 0) { SetCacheControlField(response, $"public, max-age={s}, s-maxage={s}"); } else { var minutes = s < 60 ? 0 : (s / 60); try { response.CacheControl = "no-cache"; } catch { } try { response.Expires = minutes; } catch { } AddHeader(response, "Pragma", "no-cache"); } #else if (s > 0) { AddHeader(response, "Cache-Control", $"public, max-age={s}, s-maxage={s}"); } else { var minutes = s < 60 ? 0 : (s / 60); AddHeader(response, "Cache-Control", "no-cache, no-store, must-revalidate"); AddHeader(response, "Expires", minutes.ToString()); AddHeader(response, "Pragma", "no-cache"); } #endif } public static void SetTextPlain(HttpResponse response, string value = null) { if (response == null) return; const string plain = "text/plain; charset=utf-8"; // response.ContentType = "text/plain"; // response.Charset = "utf-8"; response.ContentType = string.IsNullOrEmpty(value) ? plain : value; } public static void SetContentLength(HttpResponse response, long value) { if (response == null) return; if (value < 0L) return; #if NETFX response.AddHeader("Content-Length", value.ToString()); #else response.ContentLength = value; #endif } public static Stream GetStream(HttpResponse response) { if (response == null) return null; #if NETFX return response.OutputStream; #else return response.Body; #endif } /// 设置响应的 Cookies。 public static void SetCookies(HttpResponse response, IEnumerable> cookies) { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie // 可以是除了控制字符 (CTLs)、空格 (spaces) 或制表符 (tab)之外的任何 US-ASCII 字符。 // 同时不能包含以下分隔字符: ( ) < > @ , ; : \ " / [ ] ? = { }. // 是可选的,如果存在的话,那么需要包含在双引号里面。 // 支持除了控制字符(CTLs)、空格(whitespace)、双引号(double quotes)、逗号(comma)、分号(semicolon)以及反斜线(backslash)之外的任意 US-ASCII 字符。 // 关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 所允许使用的字符的要求是有用的。 if (cookies == null) return; var ps = new List(); foreach (var kvp in cookies) { var k = kvp.Key; var v = kvp.Value; if (k.IsEmpty()) continue; ps.Add(TextUtility.Merge(k, "="), v); } if (ps.Count < 1) return; var value = TextUtility.Merge("; ", ps); AddHeader(response, "Set-Cookie", value); } #endregion } } #endif