You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
447 lines
17 KiB
447 lines
17 KiB
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
|
|
|
|
// <cookie-name> 可以是除了控制字符 (CTLs)、空格 (spaces) 或制表符 (tab)之外的任何 US-ASCII 字符。
|
|
// 同时不能包含以下分隔字符: ( ) < > @ , ; : \ " / [ ] ? = { }.
|
|
|
|
// <cookie-value> 是可选的,如果存在的话,那么需要包含在双引号里面。
|
|
// 支持除了控制字符(CTLs)、空格(whitespace)、双引号(double quotes)、逗号(comma)、分号(semicolon)以及反斜线(backslash)之外的任意 US-ASCII 字符。
|
|
|
|
// 关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。
|
|
// 不过满足规范中对于 <cookie-value> 所允许使用的字符的要求是有用的。
|
|
|
|
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<KeyValuePair<string, string>> cookies)
|
|
{
|
|
if (cookies == null) return null;
|
|
|
|
var ps = new List<string>();
|
|
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<ApiApplication> 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<ApiFunction> 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
|
|
|
|
}
|
|
|
|
}
|
|
|