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.
725 lines
26 KiB
725 lines
26 KiB
using Apewer.Network;
|
|
using Apewer.Source;
|
|
using System;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using static Apewer.Web.ApiUtility;
|
|
|
|
namespace Apewer.Web
|
|
{
|
|
|
|
internal class ApiProcessor
|
|
{
|
|
|
|
private ApiContext _context = null;
|
|
|
|
internal ApiProcessor(ApiContext context) => _context = context ?? throw new ArgumentNullException(nameof(context));
|
|
|
|
#region prepare
|
|
|
|
/// <summary>执行处理程序,返回错误信息。</summary>
|
|
public void Run()
|
|
{
|
|
var url = null as Uri;
|
|
var method = HttpMethod.NULL;
|
|
var response = null as ApiResponse;
|
|
|
|
try
|
|
{
|
|
// 检查执行的前提条件,获取 Method 和 URL。
|
|
var check = Check(ref method, ref url);
|
|
if (!string.IsNullOrEmpty(check))
|
|
{
|
|
Logger.Internals.Error(typeof(ApiInvoker), check);
|
|
return;
|
|
}
|
|
|
|
// 准备请求模型。
|
|
var request = GetRequest(_context.Provider, _context.Options, method, url);
|
|
_context.Request = request;
|
|
|
|
// 准备响应模型。
|
|
response = new ApiResponse();
|
|
response.Random = request.Random;
|
|
response.Application = request.Application;
|
|
response.Function = request.Function;
|
|
_context.Response = response;
|
|
|
|
// 调用 API。
|
|
Invoke();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var message = ex.Message();
|
|
Logger.Internals.Error(typeof(ApiInvoker), message);
|
|
}
|
|
finally
|
|
{
|
|
// 输出。
|
|
if (response != null)
|
|
{
|
|
try
|
|
{
|
|
response.Duration = Duration(_context.Beginning);
|
|
Output(_context.Provider, _context.Options, response, null, method);
|
|
}
|
|
catch { }
|
|
finally
|
|
{
|
|
RuntimeUtility.Dispose(response.Model);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static string Duration(DateTime beginning)
|
|
{
|
|
var span = DateTime.Now - beginning;
|
|
var ms = span.TotalMilliseconds;
|
|
if (ms < 1000) return Math.Round(ms, 0).ToString() + "ms";
|
|
if (ms < 10000) return Math.Round(ms / 1000, 2).ToString() + "s";
|
|
if (ms < 60000) return Math.Round(ms / 1000, 1).ToString() + "s";
|
|
return Math.Round(ms / 1000, 0).ToString() + "s";
|
|
}
|
|
|
|
string Check(ref HttpMethod method, ref Uri url)
|
|
{
|
|
// 服务程序检查。
|
|
var check = _context.Provider.PreInvoke();
|
|
if (!string.IsNullOrEmpty(check)) return check;
|
|
|
|
// URL
|
|
url = _context.Provider.GetUrl();
|
|
if (url == null) return "URL 无效。";
|
|
|
|
method = _context.Provider.GetMethod();
|
|
if (method == HttpMethod.NULL) return "HTTP 方法无效。";
|
|
if (method == HttpMethod.OPTIONS) return null;
|
|
|
|
// favicon.ico
|
|
var lowerPath = TextUtility.AssureStarts(TextUtility.Lower(url.AbsolutePath), "/");
|
|
if (!_context.Options.AllowFavIcon)
|
|
{
|
|
if (lowerPath.StartsWith("/favicon.ico"))
|
|
{
|
|
Output(_context.Provider, _context.Options, null, null, null);
|
|
return "已取消对 favicon.ico 的请求。";
|
|
}
|
|
}
|
|
|
|
// robots.txt
|
|
if (!_context.Options.AllowRobots)
|
|
{
|
|
if (lowerPath.StartsWith("/robots.txt"))
|
|
{
|
|
const string text = "User-agent: *\nDisallow: / \n";
|
|
Output(_context.Provider, _context.Options, null, "text/plain", TextUtility.Bytes(text));
|
|
return "已取消对 robots.txt 的请求。";
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// 寻找入口。
|
|
void Invoke()
|
|
{
|
|
// 路由
|
|
if (_context.Options.UseRoute)
|
|
{
|
|
var path = _context?.Request?.Url?.AbsolutePath;
|
|
var action = _context.Entries.GetAction(path);
|
|
if (action != null)
|
|
{
|
|
_context.ApiAction = action;
|
|
Invoke(action);
|
|
_context.Response.Duration = Duration(_context.Beginning);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 反射
|
|
if (_context.Options.UseReflection)
|
|
{
|
|
var appName = _context.Request.Application;
|
|
var application = _context.Entries.GetApplication(appName);
|
|
Invoke(application);
|
|
_context.Response.Duration = Duration(_context.Beginning);
|
|
return;
|
|
}
|
|
|
|
// 未匹配到
|
|
_context.Response.Duration = Duration(_context.Beginning);
|
|
_context.Response.Model = new ApiStatusModel(404);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region common
|
|
|
|
// 创建控制器实例
|
|
static ApiController CreateController(Type type, ApiRequest request, ApiResponse response, ApiOptions options)
|
|
{
|
|
var controller = (ApiController)Activator.CreateInstance(type);
|
|
ApiUtility.SetProperties(controller, request, response, options);
|
|
return controller;
|
|
}
|
|
|
|
static void Invoke(ApiContext context, MethodInfo method, ApiParameter[] parameters)
|
|
{
|
|
context.MethodInfo = method;
|
|
|
|
// 调用。
|
|
var parametersValue = ReadParameters(context.Request, parameters);
|
|
var controller = context.Controller;
|
|
var returnValue = method.Invoke(controller, parametersValue);
|
|
|
|
// 程序要求停止输出。
|
|
var response = context.Response;
|
|
if (response.StopReturn) return;
|
|
|
|
// 已经有了返回模型。
|
|
if (response.Model != null) return;
|
|
|
|
// 没有返回类型。
|
|
var returnType = method.ReturnType;
|
|
if (returnType == null) return;
|
|
|
|
// 已明确字符串类型。
|
|
if (returnType.Equals(typeof(string)))
|
|
{
|
|
var textValue = returnValue as string;
|
|
var textRenderer = context.Options.TextRenderer;
|
|
if (textRenderer != null)
|
|
{
|
|
textRenderer.Invoke(context, textValue);
|
|
return;
|
|
}
|
|
|
|
// 默认视为提示错误
|
|
if (!string.IsNullOrEmpty(textValue)) response.Error(textValue);
|
|
return;
|
|
}
|
|
|
|
// 已明确 Exception 类型,视为提示错误。
|
|
if (returnValue is Exception)
|
|
{
|
|
ApiUtility.Exception(response, returnValue as Exception);
|
|
return;
|
|
}
|
|
|
|
// 已明确 Json 类型。
|
|
if (returnValue is Json json)
|
|
{
|
|
response.Data = json;
|
|
return;
|
|
}
|
|
|
|
// 已明确 Model 类型。
|
|
if (returnValue is IApiModel model)
|
|
{
|
|
response.Model = model;
|
|
return;
|
|
}
|
|
|
|
// 已明确 Result 类型。
|
|
if (returnValue is IActionResult result)
|
|
{
|
|
response.Model = result;
|
|
return;
|
|
}
|
|
|
|
// 类型未知,尝试 ToJson 方法。
|
|
if (returnValue is IToJson toJson)
|
|
{
|
|
response.Data = toJson.ToJson();
|
|
return;
|
|
}
|
|
|
|
// 未知返回类型,尝试使用默认渲染器。
|
|
var defaultRenderer = context.Options.DefaultRenderer;
|
|
if (defaultRenderer != null) defaultRenderer.Invoke(context, returnValue);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region route
|
|
|
|
// 执行 Action。
|
|
void Invoke(ApiAction action)
|
|
{
|
|
var controller = null as ApiController;
|
|
try
|
|
{
|
|
// 准备控制器。
|
|
controller = CreateController(action.Type, _context.Request, _context.Response, _context.Options);
|
|
|
|
// 准备参数。
|
|
var parameters = action.Parameters;
|
|
var values = ReadParameters(_context.Request, parameters);
|
|
|
|
// 调用。
|
|
_context.Controller = controller;
|
|
Invoke(_context, action.MethodInfo, action.Parameters);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.InnerException != null) ex = ex.InnerException;
|
|
ApiUtility.Exception(_context.Response, ex, _context.Options.WithException);
|
|
|
|
var catcher = _context.Invoker.Catcher;
|
|
if (catcher != null)
|
|
{
|
|
try
|
|
{
|
|
var apiCatch = new ApiCatch(_context, ex);
|
|
catcher.Invoke(apiCatch);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
RuntimeUtility.Dispose(controller);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region reflection
|
|
|
|
// 创建控制器。
|
|
void Invoke(ApiApplication application)
|
|
{
|
|
var options = _context.Options;
|
|
var entries = _context.Entries;
|
|
var request = _context.Request;
|
|
var response = _context.Response;
|
|
|
|
// Application 无效,尝试默认控制器和枚举。
|
|
if (application == null)
|
|
{
|
|
var @default = options.Default;
|
|
if (@default == null)
|
|
{
|
|
// 没有指定默认控制器,尝试枚举。
|
|
response.Status = "notfound";
|
|
response.Message = "Not Found";
|
|
if (options.AllowEnumerate) response.Data = Enumerate(entries.Applications, options);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// 创建默认控制器。
|
|
var controller = null as ApiController;
|
|
try
|
|
{
|
|
controller = CreateController(@default, request, response, options);
|
|
Invoke(controller, application, null, options, request, response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ApiUtility.Exception(response, ex.InnerException ?? ex);
|
|
}
|
|
finally
|
|
{
|
|
RuntimeUtility.Dispose(controller);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 创建控制器时候会填充 Controller.Request 属性,可能导致 Request.Function 被篡改,所以在创建之前获取 Function。
|
|
var function = application.GetFunction(request.Function);
|
|
var controller = null as ApiController;
|
|
try
|
|
{
|
|
controller = CreateController(application.Type, request, response, options);
|
|
Invoke(controller, application, function, options, request, response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ApiUtility.Exception(response, ex.InnerException ?? ex);
|
|
}
|
|
finally
|
|
{
|
|
RuntimeUtility.Dispose(controller);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 调用 Function。
|
|
void Invoke(ApiController controller, ApiApplication application, ApiFunction function, ApiOptions options, ApiRequest request, ApiResponse response)
|
|
{
|
|
try
|
|
{
|
|
// 控制器初始化。
|
|
var initializer = ApiUtility.GetInitialier(controller);
|
|
var match = initializer == null ? true : initializer.Invoke(controller);
|
|
if (!match) return;
|
|
if (application.Independent) return;
|
|
|
|
if (function != null)
|
|
{
|
|
// 调用 API,获取返回值。
|
|
_context.Controller = controller;
|
|
Invoke(_context, function.Method, function.Parameters);
|
|
}
|
|
else
|
|
{
|
|
// 未匹配到 Function,尝试 Default。
|
|
var @default = ApiUtility.GetDefault(controller);
|
|
if (@default != null)
|
|
{
|
|
@default.Invoke(controller);
|
|
return;
|
|
}
|
|
|
|
// 没有执行任何 Function,尝试枚举。
|
|
response.Status = "notfound";
|
|
if (application.Hidden)
|
|
{
|
|
response.Message = "Not Found";
|
|
}
|
|
else
|
|
{
|
|
response.Message = "Not Found";
|
|
if (options.AllowEnumerate) response.Data = Enumerate(application.Functions, options);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.InnerException != null) ex = ex.InnerException;
|
|
ApiUtility.Exception(_context.Response, ex, _context.Options.WithException);
|
|
|
|
var catcher = _context.Invoker.Catcher;
|
|
if (catcher != null)
|
|
{
|
|
try
|
|
{
|
|
var apiCatch = new ApiCatch(_context, ex);
|
|
catcher.Invoke(apiCatch);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region static
|
|
|
|
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();
|
|
var headers = provider.GetHeaders() ?? new HttpHeaders();
|
|
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 CookieCollection();
|
|
|
|
// 匹配 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 请求。
|
|
switch (request.Method)
|
|
{
|
|
case HttpMethod.PATCH:
|
|
case HttpMethod.POST:
|
|
case HttpMethod.PUT:
|
|
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();
|
|
}
|
|
}
|
|
|
|
// 尝试解析 Form,需要 application/x-www-form-urlencoded
|
|
var contentType = headers.GetValue("Content-Type") ?? "";
|
|
if (contentType.Contains("urlencoded")) request.Form = ApiUtility.Parameters(text);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// 解析 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;
|
|
}
|
|
|
|
static StringPairs PrepareHeaders(ApiOptions options, ApiResponse response, ApiRequest request = null)
|
|
{
|
|
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());
|
|
|
|
if (request != null && request.Headers != null)
|
|
{
|
|
var @private = request.Headers.GetValue("Access-Control-Request-Private-Network");
|
|
if (NumberUtility.Boolean(@private)) merged.Add("Access-Control-Allow-Private-Network", "true");
|
|
}
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
|
|
// 包含 API 的处理时间。
|
|
if (options.WithDuration && response != null)
|
|
{
|
|
if (response.Duration.NotEmpty()) merged.Add("Duration", response.Duration);
|
|
}
|
|
}
|
|
if (response != null)
|
|
{
|
|
// Cookies。
|
|
var setCookies = SetCookie(response.Cookies);
|
|
if (setCookies != null)
|
|
{
|
|
foreach (var value in setCookies) merged.Add("Set-Cookie", value);
|
|
}
|
|
|
|
// 自定义头。
|
|
var headers = response.Headers;
|
|
if (headers != null)
|
|
{
|
|
foreach (var header in headers)
|
|
{
|
|
var key = TextUtility.Trim(header.Name);
|
|
if (string.IsNullOrEmpty(key)) continue;
|
|
var value = header.Value;
|
|
if (string.IsNullOrEmpty(value)) continue;
|
|
merged.Add(key, value);
|
|
}
|
|
}
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
internal void Output(ApiProvider provider, ApiOptions options, ApiResponse response, string type, byte[] bytes)
|
|
{
|
|
var preWrite = provider.PreWrite();
|
|
if (!string.IsNullOrEmpty(preWrite)) return;
|
|
|
|
if (response != null)
|
|
{
|
|
var responsePreOutput = response.PreOutput;
|
|
if (responsePreOutput != null)
|
|
{
|
|
var @continue = responsePreOutput.Invoke(_context);
|
|
if (!@continue) return;
|
|
}
|
|
}
|
|
|
|
var invokerPreOutput = _context.Invoker.PreOutput;
|
|
if (invokerPreOutput != null)
|
|
{
|
|
var @continue = invokerPreOutput.Invoke(_context);
|
|
if (!@continue) return;
|
|
}
|
|
|
|
var optionsPreOutput = _context.Options.PreOutput;
|
|
if (optionsPreOutput != null)
|
|
{
|
|
var @continue = optionsPreOutput.Invoke(_context);
|
|
if (!@continue) return;
|
|
}
|
|
|
|
var headers = PrepareHeaders(options, response);
|
|
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, 0, bytes.Length);
|
|
provider.Sent();
|
|
}
|
|
|
|
internal void Output(ApiProvider provider, ApiOptions options, ApiResponse response, ApiRequest request, HttpMethod method)
|
|
{
|
|
var preWrite = provider.PreWrite();
|
|
if (!string.IsNullOrEmpty(preWrite)) return;
|
|
|
|
if (response != null)
|
|
{
|
|
var responsePreOutput = response.PreOutput;
|
|
if (responsePreOutput != null)
|
|
{
|
|
var @continue = responsePreOutput.Invoke(_context);
|
|
if (!@continue) return;
|
|
}
|
|
}
|
|
|
|
var invokerPreOutput = _context.Invoker.PreOutput;
|
|
if (invokerPreOutput != null)
|
|
{
|
|
var @continue = invokerPreOutput.Invoke(_context);
|
|
if (!@continue) return;
|
|
}
|
|
|
|
var optionsPreOutput = _context.Options.PreOutput;
|
|
if (optionsPreOutput != null)
|
|
{
|
|
var @continue = optionsPreOutput.Invoke(_context);
|
|
if (!@continue) return;
|
|
}
|
|
|
|
// 设置头。
|
|
var headers = PrepareHeaders(options, response, request);
|
|
foreach (var header in headers) provider.SetHeader(header.Key, header.Value);
|
|
|
|
// 自定义模型
|
|
var model = response.Model as IApiModel;
|
|
var result = response.Model as IActionResult;
|
|
if (model != null)
|
|
{
|
|
try
|
|
{
|
|
model.Output(_context);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Internals.Exception(model, ex);
|
|
}
|
|
RuntimeUtility.Dispose(model);
|
|
return;
|
|
}
|
|
else if (result != null)
|
|
{
|
|
try
|
|
{
|
|
result.ExecuteResult(_context);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Internals.Exception(result, ex);
|
|
}
|
|
RuntimeUtility.Dispose(result);
|
|
return;
|
|
}
|
|
|
|
var text = ApiUtility.ToJson(response, options);
|
|
var bytes = TextUtility.Bytes(text);
|
|
provider.SetCache(0);
|
|
provider.SetContentType("text/json; charset=utf-8");
|
|
provider.SetContentLength(bytes.Length);
|
|
var stream = provider.ResponseBody();
|
|
if (stream != null && stream.CanWrite) stream.Write(bytes, 0, bytes.Length);
|
|
provider.Sent();
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
}
|
|
|