using Apewer.Network; using Apewer.Web; using System; using System.Collections.Generic; using System.Text; namespace Apewer.Internals { internal static class ApiHelper { #region Cookies // 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 规范,这不是必须的。 // 不过满足规范中对于 所允许使用的字符的要求是有用的。 internal static StringPairs ParseCookies(StringPairs headers) { var cookies = new StringPairs(); if (headers == null) return cookies; var hvs = headers.GetValues("cookie", true, false); foreach (var hv in hvs) { if (string.IsNullOrEmpty(hv)) continue; var lines = hv.Split(';'); foreach (var line in lines) { var e = line.IndexOf("="); if (e < 0) continue; var name = TextUtility.Left(line, e, true); var value = TextUtility.Right(line, line.Length - e - 1, true); if (string.IsNullOrEmpty(name)) continue; if (string.IsNullOrEmpty(value)) continue; cookies.Add(name, value); } } return cookies; } // 生成 Response 头中 Set-Cookie 的值。 internal static string SetCookie(IEnumerable> cookies) { if (cookies == null) return null; 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 null; var value = TextUtility.Join("; ", ps); return value; } #endregion #region ApiFunction Parameterse // 为带有形参的 Function 准备实参。 internal static object[] ReadParameters(ApiRequest request, ApiFunction function) { if (request == null || function == null) return null; var pis = function.Parameters; if (pis == null) return null; var count = pis.Length; if (count < 1) return null; // 当 Function 仅有一个参数时,尝试生成模型。 if (count == 1) { if (function.ParamIsRecord) { try { var record = Activator.CreateInstance(pis[0].ParameterType); Json.Object(record, request.Data, true, null, true); return new object[] { record }; } catch { } return new object[] { null }; } // 未知类型的参数,继续使用通用方法获取。 } var ps = new object[count]; for (var i = 0; i < count; i++) { var name = pis[i].Name; var type = pis[i].ParameterType; var text = ApiUtility.Parameter(request, name); ps[i] = ReadParameter(text, type); } return ps; } static object ReadParameter(string text, Type type) { if (type.Equals(typeof(object)) || type.Equals(typeof(string))) return text; if (type.Equals(typeof(byte[]))) return TextUtility.FromBase64(text); if (type.Equals(typeof(float))) return NumberUtility.Single(text); if (type.Equals(typeof(double))) return NumberUtility.Double(text); if (type.Equals(typeof(decimal))) return NumberUtility.Decimal(text); if (type.Equals(typeof(byte))) return NumberUtility.Byte(text); if (type.Equals(typeof(sbyte))) return NumberUtility.SByte(text); if (type.Equals(typeof(short))) return NumberUtility.Int16(text); if (type.Equals(typeof(ushort))) return NumberUtility.UInt16(text); if (type.Equals(typeof(int))) return NumberUtility.Int32(text); if (type.Equals(typeof(uint))) return NumberUtility.UInt32(text); if (type.Equals(typeof(long))) return NumberUtility.Int64(text); if (type.Equals(typeof(ulong))) return NumberUtility.UInt64(text); return type.IsValueType ? Activator.CreateInstance(type) : null; } #endregion #region Enumerate Entries internal static Json Enumerate(IEnumerable applications, ApiOptions options) { var list = Json.NewArray(); var count = 0; if (applications != null) { foreach (var app in applications) { if (app == null) continue; if (app.Hidden) continue; list.AddItem(app.ToJson(options)); count = count + 1; } } var json = Json.NewObject(); json.SetProperty("count", count); json.SetProperty("list", list); return json; } internal static Json Enumerate(IEnumerable functions, ApiOptions options) { var count = 0; var list = Json.NewArray(); if (functions != null) { foreach (var func in functions) { if (func == null) continue; if (func.Hidden) continue; list.AddItem(func.ToJson(options)); count = count + 1; } } var json = Json.NewObject(); json.SetProperty("count", count); json.SetProperty("list", list); return json; } #endregion #region Request internal static ApiRequest GetRequest(ApiProvider provider, ApiOptions options, HttpMethod method, Uri url) { // 创建数据对象。 var request = new ApiRequest(); // Http Method。 request.Method = method; // 基本信息。 var ip = provider.GetClientIP() ?? null; var headers = provider.GetHeaders() ?? new StringPairs(); request.Headers = headers; request.IP = ip; request.Url = url; request.Referrer = provider.GetReferrer(); request.Parameters = ApiUtility.Parameters(url.Query); // Headers。 request.UserAgent = ApiUtility.UserAgent(headers); request.Cookies = ParseCookies(headers) ?? new StringPairs(); // 匹配 API。 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; // 解析 POST 请求。 if (request.Method == HttpMethod.POST) { var preRead = provider.PreRead(); if (string.IsNullOrEmpty(preRead)) { var post = null as byte[]; var length = 0L; var max = options.MaxRequestBody; if (max == 0) post = new byte[0]; else if (max < 0) post = provider.RequestBody().Read(); else { length = provider.GetContentLength(); if (length <= max) post = provider.RequestBody().Read(); } length = post == null ? 0 : post.Length; if (length > 1) { request.PostData = post; if (length < 104857600) { var text = TextUtility.FromBytes(post); request.PostText = text; // 尝试解析 Json,首尾必须是“{}”或“[]”。 var first = post[0]; var last = post[length - 1]; if ((first == 123 && last == 125) || (first == 91 && last == 93)) { var json = Json.From(text); if (json != null && json.IsObject) { application = json["application"]; function = json["function"]; random = json["random"]; ticket = json["ticket"]; session = json["session"]; page = json["page"]; var data = json.GetProperty("data"); request.PostJson = json; request.Data = data ?? Json.NewObject(); } } } } } } // 解析 URL 参数。 // URL 参数的优先级应高于 URL 路径,以避免反向代理产生的路径问题。 var urlParameters = ApiUtility.Parameters(request.Url.Query); if (string.IsNullOrEmpty(application)) application = urlParameters.GetValue("application"); if (string.IsNullOrEmpty(function)) function = urlParameters.GetValue("function"); if (string.IsNullOrEmpty(random)) random = urlParameters.GetValue("random"); if (string.IsNullOrEmpty(ticket)) ticket = urlParameters.GetValue("ticket"); if (string.IsNullOrEmpty(session)) session = urlParameters.GetValue("session"); if (string.IsNullOrEmpty(page)) page = urlParameters.GetValue("page"); // 从 Cookie 中获取 Ticket。 var cookies = request.Cookies; if (string.IsNullOrEmpty(ticket)) ticket = cookies.GetValue("ticket"); // 最后检查 URL 路径。 var paths = (request.Url.AbsolutePath ?? "").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 = TextUtility.Trim(application); function = TextUtility.Trim(function); random = TextUtility.Trim(random); ticket = TextUtility.Trim(ticket); session = TextUtility.Trim(session); page = TextUtility.Trim(page); // 设置请求:回传。 request.Application = application; request.Function = function; request.Random = random; // 设置请求:不回传。 request.Ticket = ticket; request.Session = session; request.Page = page; return request; } #endregion #region Response static StringPairs PrepareHeaders(ApiOptions options, ApiResponse response) { var merged = new StringPairs(); if (options != null) { // 跨域访问。 if (options.WithAccessControl) { merged.Add("Access-Control-Allow-Headers", "Content-Type"); merged.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); merged.Add("Access-Control-Allow-Origin", "*"); var maxage = options.AccessControlMaxAge; if (maxage > 0) merged.Add("Access-Control-Max-Age", maxage.ToString()); } // Content-Type 检查。 if (options.WithContentTypeOptions || options.Default != null) { merged.Add("X-Content-Type-Options", "nosniff"); } // 用于客户端,当前页面使用 HTTPS 时,将资源升级为 HTTPS。 if (options.UpgradeHttps) { merged.Add("Content-Security-Policy", "upgrade-insecure-requests"); } } if (response != null) { // Cookies。 var setCookie = SetCookie(response.Cookies); if (!string.IsNullOrEmpty(setCookie)) { merged.Add("Set-Cookie", setCookie); } // 自定义头。 var headers = response.Headers; if (headers != null) { foreach (var header in headers) { var key = TextUtility.Trim(header.Key); if (string.IsNullOrEmpty(key)) continue; var value = header.Value; if (string.IsNullOrEmpty(value)) continue; merged.Add(key, value); } } } return merged; } internal static string ExportJson(ApiResponse response, ApiOptions options) { if (response == null) return "{}"; if (string.IsNullOrEmpty(response.Status)) response.Status = "ok"; var json = Json.NewObject(); // 执行时间。 if (options.WithClock) { json.SetProperty("clock", ClockUtility.Lucid(DateTime.Now)); } // 持续时间。 if (options.WithDuration) { json.SetProperty("duration", response.Duration); } // 随机值。 var random = response.Random; if (!string.IsNullOrEmpty(random)) json.SetProperty("random", random); // 调用。 if (options.WithTarget) { json.SetProperty("application", response.Application); json.SetProperty("function", response.Function); } // 状态。 json.SetProperty("status", (TextUtility.IsBlank(response.Status) ? TextUtility.Empty : response.Status.ToLower())); if (!string.IsNullOrEmpty(response.Message)) json.SetProperty("message", response.Message); // 用户数据。 if (response.Message == "exception" && !options.WithException) json.SetProperty("data"); else json.SetProperty("data", response.Data); var indented = response.Indented || options.JsonIndent; var text = json.ToString(indented); return text; } internal static void Output(ApiProvider provider, ApiOptions options, string type, byte[] bytes) { var preWrite = provider.PreWrite(); if (!string.IsNullOrEmpty(preWrite)) return; var headers = PrepareHeaders(options, null); foreach (var header in headers) provider.SetHeader(header.Key, header.Value); provider.SetCache(0); provider.SetContentType(string.IsNullOrEmpty(type) ? "application/octet-stream" : type); var length = bytes == null ? 0 : bytes.Length; provider.SetContentLength(length); if (length > 0) provider.ResponseBody().Write(bytes); } internal static void Output(ApiProvider provider, ApiOptions options, ApiResponse response, ApiRequest request, HttpMethod method) { var preWrite = provider.PreWrite(); if (!string.IsNullOrEmpty(preWrite)) return; // 设置头。 var headers = PrepareHeaders(options, null); foreach (var header in headers) provider.SetHeader(header.Key, header.Value); var model = response.Model; if (model != null) { ApiUtility.Initialize(model, request, response, options, provider); try { model.Output(); } catch { } RuntimeUtility.Dispose(model); return; } var json = TextUtility.Bytes(ExportJson(response, options)); provider.SetCache(0); provider.SetContentType("text/json; charset=utf-8"); provider.SetContentLength(json.Length); var stream = provider.ResponseBody(); if (stream != null && stream.CanWrite) stream.Write(json); } #endregion } }