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.
659 lines
24 KiB
659 lines
24 KiB
#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<string, ApiApplication> _entries = null;
|
|
|
|
private static List<Assembly> _assemblies = null;
|
|
|
|
#endregion
|
|
|
|
#region Relection
|
|
|
|
public static Dictionary<string, ApiApplication> Entries { get { return _entries; } set { _entries = value; } }
|
|
|
|
public static List<Assembly> Assemblies { get { return _assemblies; } set { _assemblies = value; } }
|
|
|
|
internal static Dictionary<string, ApiApplication> GetEntries(IEnumerable<Assembly> assemblies, bool sort = true)
|
|
{
|
|
var entries = new Dictionary<string, ApiApplication>();
|
|
|
|
var deduplicates = new List<Assembly>();
|
|
foreach (var assembly in assemblies)
|
|
{
|
|
if (assembly == null) continue;
|
|
if (deduplicates.Contains(assembly)) continue;
|
|
deduplicates.Add(assembly);
|
|
}
|
|
_assemblies = deduplicates;
|
|
|
|
var aes = new ObjectSet<ApiApplication>();
|
|
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<ApiFunction>(true);
|
|
foreach (var method in methods)
|
|
{
|
|
var function = GetFunction(application, method);
|
|
if (function == null) continue;
|
|
fes[function.Name] = function;
|
|
}
|
|
|
|
var fns = new List<string>();
|
|
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<string>();
|
|
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<HiddenAttribute>(false))
|
|
{
|
|
entry.Visible = false;
|
|
}
|
|
}
|
|
|
|
// Independent
|
|
if (type.ContainsAttribute<IndependentAttribute>(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<string, string>(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<string, string>(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
|
|
|
|
/// <summary>设置 status 为 error,并设置 message 的内容。</summary>
|
|
internal static void RespondError(ApiResponse response, string message = "未知错误。")
|
|
{
|
|
if (response == null) return;
|
|
response.Type = ApiFormat.Json;
|
|
response.Status = "error";
|
|
response.Message = message ?? TextUtility.EmptyString;
|
|
}
|
|
|
|
/// <summary>设置 status 为 error,并设置 message 的内容。</summary>
|
|
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 { }
|
|
}
|
|
|
|
/// <summary>输出 UTF-8 文本。</summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>输出字节数组。</summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>输出二进制。</summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>输出文件。</summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>重定向。</summary>
|
|
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)
|
|
{
|
|
if (response == null) return "{}";
|
|
|
|
var json = Json.NewObject();
|
|
|
|
// 执行时间。
|
|
if (ApiOptions.WithClock)
|
|
{
|
|
json.SetProperty("clock", response.Ending.ToLucid());
|
|
}
|
|
|
|
// 持续时间。
|
|
if (ApiOptions.WithDuration)
|
|
{
|
|
var seconds = Math.Floor((response.Ending - response.Beginning).TotalMilliseconds) / 1000D;
|
|
json.SetProperty("duration", seconds);
|
|
}
|
|
|
|
// 随机值。
|
|
if (response.Random.NotEmpty()) json.SetProperty("random", response.Random);
|
|
|
|
// 调用。
|
|
if (ApiOptions.WithTarget)
|
|
{
|
|
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);
|
|
|
|
// Ticket。
|
|
if (response.Ticket != null) json.SetProperty("ticket", response.Ticket);
|
|
|
|
// 异常。
|
|
if (ApiOptions.AllowException && response.Exception != null)
|
|
{
|
|
try
|
|
{
|
|
var exMessage = null as string;
|
|
var exStackTrace = null as string;
|
|
var exSource = null as string;
|
|
var exHelpLink = null as string;
|
|
|
|
exMessage = response.Exception.Message;
|
|
exStackTrace = response.Exception.StackTrace;
|
|
exSource = response.Exception.Source;
|
|
exHelpLink = response.Exception.HelpLink;
|
|
|
|
// Exception 对象的主要属性。
|
|
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);
|
|
|
|
// WebException 附加数据。
|
|
var webex = response.Exception as System.Net.WebException;
|
|
if (webex != null) exJson.SetProperty("data", Json.Parse(webex.Data));
|
|
|
|
json.SetProperty("exception", exJson);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var exJson = Json.NewObject();
|
|
exJson.SetProperty("message", TextUtility.Merge("设置 Exception 时再次发生异常:", ex.Message));
|
|
json.SetProperty("exception", exJson);
|
|
}
|
|
}
|
|
|
|
// 用户数据。
|
|
json.SetProperty("data", response.Data);
|
|
|
|
var text = json.ToString(ApiOptions.JsonIndent);
|
|
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);
|
|
}
|
|
}
|
|
|
|
public static void SetCORS(HttpResponse response)
|
|
{
|
|
AddHeader(response, "Access-Control-Allow-Headers", "Content-Type");
|
|
AddHeader(response, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
AddHeader(response, "Access-Control-Allow-Origin", "*");
|
|
|
|
var maxage = ApiOptions.AccessControlMaxAge;
|
|
if (maxage > 0) AddHeader(response, "Access-Control-Max-Age", maxage.ToString());
|
|
}
|
|
|
|
#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
|
|
|
|
/// <summary>设置缓存时间,单位为秒,最大为 2592000 秒(30 天)。</summary>
|
|
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)
|
|
{
|
|
response.CacheControl = "public";
|
|
response.Cache.SetCacheability(HttpCacheability.Public);
|
|
response.Cache.SetMaxAge(TimeSpan.FromSeconds(seconds));
|
|
response.Cache.SetProxyMaxAge(TimeSpan.FromSeconds(seconds));
|
|
response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
|
|
}
|
|
else
|
|
{
|
|
var minutes = s < 60 ? 0 : (s / 60);
|
|
response.CacheControl = "no-cache";
|
|
response.Cache.SetCacheability(HttpCacheability.NoCache);
|
|
response.Cache.SetNoStore();
|
|
// 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 SetContentType(HttpResponse response, string value)
|
|
{
|
|
var text = value.SafeTrim();
|
|
if (text.IsEmpty()) text = "application/octet-stream";
|
|
response.ContentType = text;
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/// <summary>设置响应的 Cookies。</summary>
|
|
public static void SetCookies(HttpResponse response, IEnumerable<KeyValuePair<string, string>> 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
|
|
|
|
// <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> 所允许使用的字符的要求是有用的。
|
|
|
|
if (cookies == null) return;
|
|
|
|
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;
|
|
|
|
var value = TextUtility.Merge("; ", ps);
|
|
AddHeader(response, "Set-Cookie", value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|