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.
519 lines
19 KiB
519 lines
19 KiB
#if NETFX || NETCORE
|
|
|
|
using Apewer;
|
|
using Apewer.Network;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
|
|
#if NETFX
|
|
using System.Web;
|
|
#endif
|
|
|
|
#if NETCORE
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.Extensions.Hosting;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
#endif
|
|
|
|
namespace Apewer.Web
|
|
{
|
|
|
|
/// <summary>API 调用器。</summary>
|
|
public sealed class ApiInvoker
|
|
{
|
|
|
|
private DateTime Beginning { get; set; }
|
|
|
|
private DateTime Ending { get; set; }
|
|
|
|
private ApiRequest ApiRequest { get; set; }
|
|
|
|
private ApiResponse ApiResponse { get; set; }
|
|
|
|
private List<Assembly> Assemblies { get; set; }
|
|
|
|
private HttpContext Context { get; set; }
|
|
|
|
private Dictionary<string, ApiApplication> Entries { get; set; }
|
|
|
|
private bool CheckDependents()
|
|
{
|
|
if (Context == null)
|
|
{
|
|
#if NETFX
|
|
Context = HttpContext.Current;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
// Fix Entries & Assemblies
|
|
if (Entries == null)
|
|
{
|
|
if (ApiInternals.Entries == null)
|
|
{
|
|
if (Assemblies == null) Assemblies = new List<Assembly>();
|
|
ApiInternals.Entries = ApiInternals.GetEntries(Assemblies, true);
|
|
}
|
|
Entries = ApiInternals.Entries;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool CheckMethod()
|
|
{
|
|
var method = WebUtility.GetMethod(Context.Request);
|
|
switch (method)
|
|
{
|
|
case HttpMethod.GET:
|
|
case HttpMethod.POST:
|
|
return true;
|
|
case HttpMethod.OPTIONS:
|
|
default:
|
|
return false; // 返回空内容;
|
|
}
|
|
}
|
|
|
|
private bool CheckFavIcon()
|
|
{
|
|
if (ApiOptions.AllowFavIcon) return true;
|
|
|
|
const string path = "/favicon.ico";
|
|
#if NETFX
|
|
if (Context.Request.Url.AbsolutePath.ToLower().StartsWith(path)) return false;
|
|
#else
|
|
if (TextUtility.ToLower(Context.Request.Path).StartsWith(path)) return false;
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
private bool CheckRobot()
|
|
{
|
|
if (ApiOptions.AllowRobot) return true;
|
|
|
|
const string path = "/robot.txt";
|
|
const string text = "User-agent: *\nDisallow: / \n";
|
|
var bytes = TextUtility.ToBinary(text);
|
|
#if NETFX
|
|
if (Context.Request.Url.AbsolutePath.ToLower().StartsWith(path))
|
|
{
|
|
Context.Response.ContentType = "text/plain";
|
|
BinaryUtility.Write(Context.Response.OutputStream, bytes);
|
|
return false;
|
|
}
|
|
#else
|
|
if (TextUtility.ToLower(Context.Request.Path).StartsWith(path))
|
|
{
|
|
Context.Response.ContentType = "text/plain";
|
|
Context.Response.ContentLength = bytes.Length;
|
|
BinaryUtility.Write(Context.Response.Body, bytes);
|
|
return false;
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
private string Run()
|
|
{
|
|
// 检查依赖的属性,若不通过,则无法执行。
|
|
if (!CheckDependents()) return "缺少必要属性。";
|
|
|
|
// 过滤请求。
|
|
if (!CheckMethod()) return "已阻止方法。";
|
|
if (!CheckFavIcon()) return "已阻止请求 favicon.ico 路径。";
|
|
if (!CheckRobot()) return "已阻止请求 robot.txt 路径。";
|
|
|
|
// 准备变量。
|
|
ApiRequest = ApiInternals.GetRequest(Context.Request);
|
|
ApiRequest.Context = Context;
|
|
ApiResponse = new ApiResponse();
|
|
ApiResponse.Context = Context;
|
|
|
|
// 准备向 Controller 传递的属性。
|
|
var an = ApiRequest.Application;
|
|
var ae = null as ApiApplication;
|
|
var fn = ApiRequest.Function;
|
|
var fe = null as ApiFunction;
|
|
var r = ApiRequest.Random;
|
|
var t = ApiRequest.Ticket;
|
|
|
|
// 调用。
|
|
if (Entries.ContainsKey(an))
|
|
{
|
|
ae = Entries[an];
|
|
if (ae.Functions.ContainsKey(fn))
|
|
{
|
|
fe = ae.Functions[fn];
|
|
Invoke(ae, fe, ApiRequest, ApiResponse);
|
|
}
|
|
else
|
|
{
|
|
if (ae.Independent)
|
|
{
|
|
Invoke(ae, null, ApiRequest, ApiResponse);
|
|
}
|
|
else
|
|
{
|
|
ApiResponse.Error("未指定有效的 Function 名称。");
|
|
if (fn.IsEmpty() && ApiOptions.AllowNumerate) Enumerate(ae.Functions);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ApiResponse.Error("未指定有效的 Application 名称。");
|
|
if (an.IsEmpty() && ApiOptions.AllowNumerate) Enumerate(Entries);
|
|
}
|
|
|
|
// 记录结束时间。
|
|
Ending = DateTime.Now;
|
|
|
|
// 调整响应。
|
|
ApiResponse.Beginning = Beginning.ToLucid();
|
|
ApiResponse.Ending = Ending.ToLucid();
|
|
ApiResponse.Application = an;
|
|
ApiResponse.Function = fn;
|
|
ApiResponse.Random = r;
|
|
|
|
// 向客户端输出。
|
|
return Output();
|
|
}
|
|
|
|
private void Enumerate(Dictionary<string, ApiApplication> applications)
|
|
{
|
|
var count = 0;
|
|
var list = Json.NewArray();
|
|
if (applications != null)
|
|
{
|
|
foreach (var i in applications)
|
|
{
|
|
if (!i.Value.Visible) continue;
|
|
var item = Json.NewObject();
|
|
item["name"] = i.Value.Name;
|
|
item["caption"] = i.Value.Caption;
|
|
if (ApiOptions.ShowModule) item["module"] = i.Value.Module;
|
|
if (ApiOptions.ShowClass) item["class"] = i.Value.Type.FullName;
|
|
list.AddItem(item);
|
|
count = count + 1;
|
|
}
|
|
}
|
|
ApiResponse.Data.Reset(Json.NewObject());
|
|
ApiResponse.Data.SetProperty("count", count);
|
|
ApiResponse.Data.SetProperty("list", list);
|
|
}
|
|
|
|
private void Enumerate(Dictionary<string, ApiFunction> functions)
|
|
{
|
|
var count = 0;
|
|
var list = Json.NewArray();
|
|
if (functions != null)
|
|
{
|
|
foreach (var i in functions)
|
|
{
|
|
if (!i.Value.Visible) continue;
|
|
var item = Json.NewObject();
|
|
item["name"] = i.Value.Name;
|
|
item["caption"] = i.Value.Caption;
|
|
item["description"] = i.Value.Description;
|
|
list.AddItem(item);
|
|
count = count + 1;
|
|
}
|
|
}
|
|
ApiResponse.Data.Reset(Json.NewObject());
|
|
ApiResponse.Data.SetProperty("count", count);
|
|
ApiResponse.Data.SetProperty("list", list);
|
|
}
|
|
|
|
private void Invoke(ApiApplication application, ApiFunction function, ApiRequest request, ApiResponse response)
|
|
{
|
|
if (request == null || response == null) return;
|
|
|
|
// 创建应用程序实例。
|
|
var target = null as ApiController;
|
|
try
|
|
{
|
|
if (application == null)
|
|
{
|
|
response.Error("指定的 Application 无效,无法创建实例。");
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
//var instance = assembly.CreateInstance(argApplication.FullName, false);
|
|
//var handler = Activator.CreateInstance(null, argApplication.FullName);
|
|
//target = (Application)handler.Unwrap();
|
|
target = (ApiController)Activator.CreateInstance(application.Type);
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
response.Error(exception);
|
|
return;
|
|
}
|
|
|
|
// 调用功能。
|
|
try
|
|
{
|
|
target.Request = request;
|
|
target.Response = response;
|
|
target.Context = Context;
|
|
|
|
if (target.AfterInitialized != null) target.AfterInitialized.Invoke();
|
|
|
|
var allowed = target.AllowFunction;
|
|
if (application.Independent) allowed = false;
|
|
|
|
if (allowed)
|
|
{
|
|
if (function == null)
|
|
{
|
|
response.Error("指定的 Application 不包含有效 Function。");
|
|
}
|
|
else
|
|
{
|
|
var result = function.Method.Invoke(target, null);
|
|
target.Dispose();
|
|
if (response.Status.IsBlank()) response.Status = "ok";
|
|
if (function.Returnable && result != null)
|
|
{
|
|
if (result is string)
|
|
{
|
|
var error = (string)result;
|
|
if (!string.IsNullOrEmpty(error)) response.Error(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
response.Error(exception.InnerException);
|
|
}
|
|
try { target.Dispose(); } catch { }
|
|
}
|
|
|
|
private string Output()
|
|
{
|
|
if (Context == null) return "Invoker 的 Context 无效。";
|
|
if (ApiResponse == null) return "Invoker 的 ApiResponse 无效。";
|
|
|
|
try
|
|
{
|
|
var response = Context.Response;
|
|
var body = ApiInternals.GetStream(response);
|
|
ApiInternals.AddHeaders(response, ApiResponse.Headers);
|
|
switch (ApiResponse.Type)
|
|
{
|
|
case ApiFormat.Json:
|
|
{
|
|
var text = ApiInternals.ExportJson(ApiResponse, ApiOptions.JsonIndent, ApiOptions.AllowException);
|
|
var data = TextUtility.ToBinary(text);
|
|
ApiInternals.SetTextPlain(response);
|
|
ApiInternals.SetContentLength(response, data.LongLength);
|
|
body.Write(data);
|
|
}
|
|
break;
|
|
case ApiFormat.Text:
|
|
{
|
|
var data = TextUtility.ToBinary(ApiResponse.TextString);
|
|
var type = ApiResponse.TextType;
|
|
ApiInternals.SetTextPlain(response, type);
|
|
ApiInternals.SetContentLength(response, data.LongLength);
|
|
body.Write(data);
|
|
}
|
|
break;
|
|
case ApiFormat.Binary:
|
|
{
|
|
Context.Response.ContentType = ApiResponse.BinaryType ?? "application/octet-stream";
|
|
if (ApiResponse.BinaryBytes != null)
|
|
{
|
|
var data = ApiResponse.BinaryBytes;
|
|
var length = data.LongLength;
|
|
ApiInternals.SetContentLength(response, length);
|
|
if (length > 0L) body.Write(data);
|
|
}
|
|
else if (ApiResponse.BinaryStream != null && ApiResponse.BinaryStream.CanRead)
|
|
{
|
|
var source = ApiResponse.BinaryStream;
|
|
try
|
|
{
|
|
var length = source.Length - source.Position;
|
|
ApiInternals.SetContentLength(response, length);
|
|
if (length > 0) body.Write(source);
|
|
}
|
|
catch { }
|
|
KernelUtility.Dispose(source);
|
|
}
|
|
}
|
|
break;
|
|
case ApiFormat.File:
|
|
{
|
|
var type = ApiResponse.FileType ?? "application/octet-stream";
|
|
var name = TextUtility.EncodeUrl(ApiResponse.FileName);
|
|
var disposition = TextUtility.Merge("attachment; filename=", name);
|
|
|
|
response.ContentType = type;
|
|
ApiInternals.AddHeader(response, "Content-Disposition", disposition);
|
|
|
|
var source = ApiResponse.FileStream;
|
|
try
|
|
{
|
|
if (source != null && source.CanRead)
|
|
{
|
|
var length = source.Length - source.Position;
|
|
ApiInternals.SetContentLength(response, length);
|
|
if (length > 0) body.Write(source);
|
|
}
|
|
}
|
|
catch { }
|
|
KernelUtility.Dispose(source);
|
|
}
|
|
break;
|
|
case ApiFormat.Redirect:
|
|
{
|
|
WebUtility.RedirectResponse(response, ApiResponse.RedirectUrl);
|
|
}
|
|
break;
|
|
}
|
|
// Context.Response.Flush();
|
|
// Context.Response.End();
|
|
}
|
|
catch (Exception ex) { return ex.Message; }
|
|
return null;
|
|
}
|
|
|
|
private ApiInvoker()
|
|
{
|
|
Beginning = DateTime.Now;
|
|
}
|
|
|
|
/// <summary>获取 ApiServer 派生类中定义的 WebAPI 入口。</summary>
|
|
public static Dictionary<string, ApiApplication> GetEntries(IEnumerable<Assembly> assemblies, bool sort = false)
|
|
{
|
|
return ApiInternals.GetEntries(assemblies, sort);
|
|
}
|
|
|
|
/// <summary>解析请求,并根据执行。</summary>
|
|
public static void Execute(Dictionary<string, ApiApplication> entries)
|
|
{
|
|
var invoker = new ApiInvoker();
|
|
invoker.Entries = entries;
|
|
invoker.Run();
|
|
}
|
|
|
|
#if NETFX
|
|
|
|
/// <summary>解析请求,获取指定程序集中定义的入口,并执行。指定程序集为 NULL 值时,将自动从应用程序域获取所有 WebAPI 入口。</summary>
|
|
public static void Execute(Assembly assembly = null)
|
|
{
|
|
if (assembly == null)
|
|
{
|
|
if (ApiInternals.Assemblies == null || ApiInternals.Assemblies.Count < 1)
|
|
{
|
|
var assemblies = new List<Assembly>();
|
|
assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies());
|
|
|
|
var calling = Assembly.GetCallingAssembly();
|
|
var contains = false;
|
|
foreach (var a in assemblies)
|
|
{
|
|
if (a.FullName == calling.FullName)
|
|
{
|
|
contains = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!contains) assemblies.Add(calling);
|
|
|
|
ApiInternals.Assemblies = assemblies;
|
|
}
|
|
if (ApiInternals.Entries == null || ApiInternals.Entries.Count < 1)
|
|
{
|
|
var entries = ApiInternals.GetEntries(ApiInternals.Assemblies, true);
|
|
ApiInternals.Entries = entries;
|
|
}
|
|
Execute(ApiInternals.Entries);
|
|
}
|
|
else
|
|
{
|
|
var assemblies = new List<Assembly>();
|
|
assemblies.Add(assembly);
|
|
|
|
var invoker = new ApiInvoker();
|
|
invoker.Entries = ApiInternals.GetEntries(assemblies, true);
|
|
invoker.Run();
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
#if NETCORE
|
|
|
|
private static Dictionary<string, ApiApplication> KestrelEntries = null;
|
|
|
|
/// <summary>设置 Kestrel 的 API 入口。</summary>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
public static void SetKestrelEntries(IEnumerable<Assembly> assemblies, bool sort = true)
|
|
{
|
|
if (assemblies == null) throw new ArgumentNullException(nameof(assemblies));
|
|
KestrelEntries = ApiInternals.GetEntries(assemblies, sort);
|
|
}
|
|
|
|
/// <summary>设置 Kestrel 的 API 入口。</summary>
|
|
public static void SetKestrelEntries(Assembly assembly, bool sort = true)
|
|
{
|
|
var assemblies = new Assembly[] { assembly };
|
|
if (assemblies[0] == null) assemblies[0] = Assembly.GetCallingAssembly();
|
|
KestrelEntries = ApiInternals.GetEntries(assemblies, sort);
|
|
}
|
|
|
|
// internal static async Task Execute(HttpContext context)
|
|
// {
|
|
// Invoke(context, ApiInternals.Entries);
|
|
// await context.Response.StartAsync();
|
|
// }
|
|
|
|
internal static string Execute(HttpContext context)
|
|
{
|
|
var invoker = new ApiInvoker();
|
|
invoker.Context = context;
|
|
invoker.Entries = KestrelEntries;
|
|
return invoker.Run();
|
|
}
|
|
|
|
/// <summary>运行 Kestrel 服务器,可指定端口和最大请求 Body 长度。默认同步运行,阻塞当前线程。</summary>
|
|
/// <remarks>注意:启动前应使用 SetKestrelEntries 方法设置 Kestrel Entries。</remarks>
|
|
public static IHost RunKestrel(int port = 80, int request = 1073741824, bool async = false)
|
|
{
|
|
if (KestrelEntries == null) SetKestrelEntries(Assembly.GetCallingAssembly());
|
|
|
|
var builder1 = Host.CreateDefaultBuilder();
|
|
var builder2 = builder1.ConfigureWebHostDefaults((builder3) =>
|
|
{
|
|
var builder4 = builder3.ConfigureKestrel((options) =>
|
|
{
|
|
options.ListenAnyIP(port);
|
|
options.AllowSynchronousIO = true;
|
|
if (request > 0) options.Limits.MaxRequestBodySize = request;
|
|
});
|
|
var builder5 = builder4.UseStartup<ApiStartup>();
|
|
});
|
|
var built = builder2.Build();
|
|
|
|
if (async) built.RunAsync();
|
|
else built.Run();
|
|
return built;
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|