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.

742 lines
27 KiB

3 years ago
8 months ago
3 years ago
2 years ago
8 months ago
2 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
2 years ago
8 months ago
3 years ago
2 years ago
8 months ago
2 years ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
2 years ago
2 years ago
2 years ago
2 years ago
8 months ago
2 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
8 months ago
3 years ago
8 months ago
8 months ago
6 months ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
8 months ago
8 months ago
3 years ago
8 months ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
2 years ago
3 years ago
8 months ago
2 years ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
8 months ago
3 years ago
2 years ago
3 years ago
8 months ago
3 years ago
2 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
3 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
8 months ago
3 years ago
8 months ago
8 months ago
3 years ago
8 months ago
8 months ago
3 years ago
2 years ago
3 years ago
  1. using Apewer.Network;
  2. using Apewer.Source;
  3. using System;
  4. using System.Net;
  5. using System.Reflection;
  6. using static Apewer.Web.ApiUtility;
  7. namespace Apewer.Web
  8. {
  9. internal class ApiProcessor
  10. {
  11. private ApiContext _context = null;
  12. internal ApiProcessor(ApiContext context) => _context = context ?? throw new ArgumentNullException(nameof(context));
  13. #region prepare
  14. /// <summary>执行处理程序,返回错误信息。</summary>
  15. public void Run()
  16. {
  17. var url = null as Uri;
  18. var method = HttpMethod.NULL;
  19. var response = null as ApiResponse;
  20. try
  21. {
  22. // 检查执行的前提条件,获取 Method 和 URL。
  23. var check = Check(ref method, ref url);
  24. if (!string.IsNullOrEmpty(check))
  25. {
  26. Logger.Internals.Error(typeof(ApiInvoker), check);
  27. return;
  28. }
  29. // 准备请求模型。
  30. var request = GetRequest(_context.Provider, _context.Options, method, url);
  31. _context.Request = request;
  32. // 准备响应模型。
  33. response = new ApiResponse();
  34. response.Random = request.Random;
  35. response.Application = request.Application;
  36. response.Function = request.Function;
  37. _context.Response = response;
  38. // 调用 API。
  39. Invoke();
  40. }
  41. catch (Exception ex)
  42. {
  43. var message = ex.Message();
  44. Logger.Internals.Error(typeof(ApiInvoker), message);
  45. }
  46. finally
  47. {
  48. // 输出。
  49. if (response != null)
  50. {
  51. try
  52. {
  53. response.Duration = Duration(_context.Beginning);
  54. Output(_context.Provider, _context.Options, response, null, method);
  55. }
  56. catch { }
  57. finally
  58. {
  59. RuntimeUtility.Dispose(response.Model);
  60. }
  61. }
  62. }
  63. }
  64. static string Duration(DateTime beginning)
  65. {
  66. var span = DateTime.Now - beginning;
  67. var ms = span.TotalMilliseconds;
  68. if (ms < 1000) return Math.Round(ms, 0).ToString() + "ms";
  69. if (ms < 10000) return Math.Round(ms / 1000, 2).ToString() + "s";
  70. if (ms < 60000) return Math.Round(ms / 1000, 1).ToString() + "s";
  71. return Math.Round(ms / 1000, 0).ToString() + "s";
  72. }
  73. string Check(ref HttpMethod method, ref Uri url)
  74. {
  75. // 服务程序检查。
  76. var check = _context.Provider.PreInvoke();
  77. if (!string.IsNullOrEmpty(check)) return check;
  78. // URL
  79. url = _context.Provider.GetUrl();
  80. if (url == null) return "URL 无效。";
  81. method = _context.Provider.GetMethod();
  82. if (method == HttpMethod.NULL) return "HTTP 方法无效。";
  83. if (method == HttpMethod.OPTIONS) return null;
  84. // favicon.ico
  85. var lowerPath = TextUtility.AssureStarts(TextUtility.Lower(url.AbsolutePath), "/");
  86. if (!_context.Options.AllowFavIcon)
  87. {
  88. if (lowerPath.StartsWith("/favicon.ico"))
  89. {
  90. Output(_context.Provider, _context.Options, null, null, null);
  91. return "已取消对 favicon.ico 的请求。";
  92. }
  93. }
  94. // robots.txt
  95. if (!_context.Options.AllowRobots)
  96. {
  97. if (lowerPath.StartsWith("/robots.txt"))
  98. {
  99. const string text = "User-agent: *\nDisallow: / \n";
  100. Output(_context.Provider, _context.Options, null, "text/plain", TextUtility.Bytes(text));
  101. return "已取消对 robots.txt 的请求。";
  102. }
  103. }
  104. return null;
  105. }
  106. // 寻找入口。
  107. void Invoke()
  108. {
  109. // 路由
  110. if (_context.Options.UseRoute)
  111. {
  112. var path = _context?.Request?.Url?.AbsolutePath;
  113. path = path.TrimEnd('/');
  114. var action = _context.Entries.GetAction(path);
  115. if (action != null)
  116. {
  117. _context.ApiAction = action;
  118. Invoke(action);
  119. _context.Response.Duration = Duration(_context.Beginning);
  120. return;
  121. }
  122. }
  123. // 反射
  124. if (_context.Options.UseReflection)
  125. {
  126. var appName = _context.Request.Application;
  127. var application = _context.Entries.GetApplication(appName);
  128. Invoke(application);
  129. _context.Response.Duration = Duration(_context.Beginning);
  130. return;
  131. }
  132. // 未匹配到
  133. _context.Response.Duration = Duration(_context.Beginning);
  134. _context.Response.Model = new ApiStatusModel(404);
  135. }
  136. #endregion
  137. #region common
  138. // 创建控制器实例
  139. static ApiController CreateController(Type type, ApiRequest request, ApiResponse response, ApiOptions options)
  140. {
  141. var controller = (ApiController)Activator.CreateInstance(type);
  142. ApiUtility.SetProperties(controller, request, response, options);
  143. return controller;
  144. }
  145. static void Invoke(ApiContext context, MethodInfo method, ApiParameter[] parameters)
  146. {
  147. context.MethodInfo = method;
  148. // 调用。
  149. var parametersValue = ReadParameters(context.Request, parameters);
  150. var controller = context.Controller;
  151. var returnValue = method.Invoke(controller, parametersValue);
  152. // 程序要求停止输出。
  153. var response = context.Response;
  154. if (response.StopReturn) return;
  155. // 已经有了返回模型。
  156. if (response.Model != null) return;
  157. // 没有返回类型。
  158. var returnType = method.ReturnType;
  159. if (returnType == null) return;
  160. // 已明确字符串类型。
  161. if (returnType.Equals(typeof(string)))
  162. {
  163. var textValue = returnValue as string;
  164. var textRenderer = context.Options.TextRenderer;
  165. if (textRenderer != null)
  166. {
  167. textRenderer.Invoke(context, textValue);
  168. return;
  169. }
  170. // 默认视为提示错误
  171. if (!string.IsNullOrEmpty(textValue)) response.Error(textValue);
  172. return;
  173. }
  174. // 已明确 Exception 类型,视为提示错误。
  175. if (returnValue is Exception)
  176. {
  177. ApiUtility.Exception(response, returnValue as Exception);
  178. return;
  179. }
  180. // 已明确 Json 类型。
  181. if (returnValue is Json json)
  182. {
  183. var renderer = context.Options.JsonRenderer;
  184. if (renderer != null)
  185. {
  186. renderer.Invoke(context, json);
  187. return;
  188. }
  189. // 默认设置到 data 属性。
  190. response.Data = json;
  191. return;
  192. }
  193. // 已明确 Model 类型。
  194. if (returnValue is IApiModel model)
  195. {
  196. response.Model = model;
  197. return;
  198. }
  199. // 已明确 Result 类型。
  200. if (returnValue is IActionResult result)
  201. {
  202. response.Model = result;
  203. return;
  204. }
  205. // 类型未知,尝试 ToJson 方法。
  206. if (returnValue is IToJson toJson)
  207. {
  208. var tojson = toJson.ToJson();
  209. var renderer = context.Options.JsonRenderer;
  210. if (renderer != null)
  211. {
  212. renderer.Invoke(context, tojson);
  213. return;
  214. }
  215. response.Data = tojson;
  216. return;
  217. }
  218. // 未知返回类型,尝试使用默认渲染器。
  219. var defaultRenderer = context.Options.DefaultRenderer;
  220. if (defaultRenderer != null) defaultRenderer.Invoke(context, returnValue);
  221. }
  222. #endregion
  223. #region route
  224. // 执行 Action。
  225. void Invoke(ApiAction action)
  226. {
  227. var controller = null as ApiController;
  228. try
  229. {
  230. // 准备控制器。
  231. controller = CreateController(action.Type, _context.Request, _context.Response, _context.Options);
  232. // 准备参数。
  233. var parameters = action.Parameters;
  234. var values = ReadParameters(_context.Request, parameters);
  235. // 调用。
  236. _context.Controller = controller;
  237. Invoke(_context, action.MethodInfo, action.Parameters);
  238. }
  239. catch (Exception ex)
  240. {
  241. if (ex.InnerException != null) ex = ex.InnerException;
  242. ApiUtility.Exception(_context.Response, ex, _context.Options.WithException);
  243. var catcher = _context.Invoker.Catcher;
  244. if (catcher != null)
  245. {
  246. try
  247. {
  248. var apiCatch = new ApiCatch(_context, ex);
  249. catcher.Invoke(apiCatch);
  250. }
  251. catch { }
  252. }
  253. }
  254. finally
  255. {
  256. RuntimeUtility.Dispose(controller);
  257. }
  258. }
  259. #endregion
  260. #region reflection
  261. // 创建控制器。
  262. void Invoke(ApiApplication application)
  263. {
  264. var options = _context.Options;
  265. var entries = _context.Entries;
  266. var request = _context.Request;
  267. var response = _context.Response;
  268. // Application 无效,尝试默认控制器和枚举。
  269. if (application == null)
  270. {
  271. var @default = options.Default;
  272. if (@default == null)
  273. {
  274. // 没有指定默认控制器,尝试枚举。
  275. response.Status = "notfound";
  276. response.Message = "Not Found";
  277. if (options.AllowEnumerate) response.Data = Enumerate(entries.Applications, options);
  278. return;
  279. }
  280. else
  281. {
  282. // 创建默认控制器。
  283. var controller = null as ApiController;
  284. try
  285. {
  286. controller = CreateController(@default, request, response, options);
  287. Invoke(controller, application, null, options, request, response);
  288. }
  289. catch (Exception ex)
  290. {
  291. ApiUtility.Exception(response, ex.InnerException ?? ex);
  292. }
  293. finally
  294. {
  295. RuntimeUtility.Dispose(controller);
  296. }
  297. }
  298. }
  299. else
  300. {
  301. // 创建控制器时候会填充 Controller.Request 属性,可能导致 Request.Function 被篡改,所以在创建之前获取 Function。
  302. var function = application.GetFunction(request.Function);
  303. var controller = null as ApiController;
  304. try
  305. {
  306. controller = CreateController(application.Type, request, response, options);
  307. Invoke(controller, application, function, options, request, response);
  308. }
  309. catch (Exception ex)
  310. {
  311. ApiUtility.Exception(response, ex.InnerException ?? ex);
  312. }
  313. finally
  314. {
  315. RuntimeUtility.Dispose(controller);
  316. }
  317. }
  318. }
  319. // 调用 Function。
  320. void Invoke(ApiController controller, ApiApplication application, ApiFunction function, ApiOptions options, ApiRequest request, ApiResponse response)
  321. {
  322. try
  323. {
  324. // 控制器初始化。
  325. var initializer = ApiUtility.GetInitialier(controller);
  326. var match = initializer == null ? true : initializer.Invoke(controller);
  327. if (!match) return;
  328. if (application.Independent) return;
  329. if (function != null)
  330. {
  331. // 调用 API,获取返回值。
  332. _context.Controller = controller;
  333. Invoke(_context, function.Method, function.Parameters);
  334. }
  335. else
  336. {
  337. // 未匹配到 Function,尝试 Default。
  338. var @default = ApiUtility.GetDefault(controller);
  339. if (@default != null)
  340. {
  341. @default.Invoke(controller);
  342. return;
  343. }
  344. // 没有执行任何 Function,尝试枚举。
  345. response.Status = "notfound";
  346. if (application.Hidden)
  347. {
  348. response.Message = "Not Found";
  349. }
  350. else
  351. {
  352. response.Message = "Not Found";
  353. if (options.AllowEnumerate) response.Data = Enumerate(application.Functions, options);
  354. }
  355. }
  356. }
  357. catch (Exception ex)
  358. {
  359. if (ex.InnerException != null) ex = ex.InnerException;
  360. ApiUtility.Exception(_context.Response, ex, _context.Options.WithException);
  361. var catcher = _context.Invoker.Catcher;
  362. if (catcher != null)
  363. {
  364. try
  365. {
  366. var apiCatch = new ApiCatch(_context, ex);
  367. catcher.Invoke(apiCatch);
  368. }
  369. catch { }
  370. }
  371. }
  372. }
  373. #endregion
  374. #region static
  375. internal static ApiRequest GetRequest(ApiProvider provider, ApiOptions options, HttpMethod method, Uri url)
  376. {
  377. // 创建数据对象。
  378. var request = new ApiRequest();
  379. // Http Method。
  380. request.Method = method;
  381. // 基本信息。
  382. var ip = provider.GetClientIP();
  383. var headers = provider.GetHeaders() ?? new HttpHeaders();
  384. request.Headers = headers;
  385. request.IP = ip;
  386. request.Url = url;
  387. request.Referrer = provider.GetReferrer();
  388. request.Parameters = ApiUtility.Parameters(url.Query);
  389. // Headers。
  390. request.UserAgent = ApiUtility.UserAgent(headers);
  391. request.Cookies = ParseCookies(headers) ?? new CookieCollection();
  392. // 匹配 API。
  393. var application = null as string;
  394. var function = null as string;
  395. var random = null as string;
  396. var ticket = null as string;
  397. var session = null as string;
  398. var page = null as string;
  399. // 解析 POST 请求。
  400. switch (request.Method)
  401. {
  402. case HttpMethod.PATCH:
  403. case HttpMethod.POST:
  404. case HttpMethod.PUT:
  405. var preRead = provider.PreRead();
  406. if (string.IsNullOrEmpty(preRead))
  407. {
  408. var post = null as byte[];
  409. var length = 0L;
  410. var max = options.MaxRequestBody;
  411. if (max == 0) post = new byte[0];
  412. else if (max < 0) post = provider.RequestBody().Read();
  413. else
  414. {
  415. length = provider.GetContentLength();
  416. if (length <= max) post = provider.RequestBody().Read();
  417. }
  418. length = post == null ? 0 : post.Length;
  419. if (length > 1)
  420. {
  421. request.PostData = post;
  422. if (length < 104857600)
  423. {
  424. var text = TextUtility.FromBytes(post);
  425. request.PostText = text;
  426. // 尝试解析 Json,首尾必须是“{}”或“[]”。
  427. var first = post[0];
  428. var last = post[length - 1];
  429. if ((first == 123 && last == 125) || (first == 91 && last == 93))
  430. {
  431. var json = Json.From(text);
  432. if (json != null && json.IsObject)
  433. {
  434. application = json["application"];
  435. function = json["function"];
  436. random = json["random"];
  437. ticket = json["ticket"];
  438. session = json["session"];
  439. page = json["page"];
  440. var data = json.GetProperty("data");
  441. request.PostJson = json;
  442. request.Data = data ?? Json.NewObject();
  443. }
  444. }
  445. // 尝试解析 Form,需要 application/x-www-form-urlencoded
  446. var contentType = headers.GetValue("Content-Type") ?? "";
  447. if (contentType.Contains("urlencoded")) request.Form = ApiUtility.Parameters(text);
  448. }
  449. }
  450. }
  451. break;
  452. }
  453. // 解析 URL 参数。
  454. // URL 参数的优先级应高于 URL 路径,以避免反向代理产生的路径问题。
  455. var urlParameters = ApiUtility.Parameters(request.Url.Query);
  456. if (string.IsNullOrEmpty(application)) application = urlParameters.GetValue("application");
  457. if (string.IsNullOrEmpty(function)) function = urlParameters.GetValue("function");
  458. if (string.IsNullOrEmpty(random)) random = urlParameters.GetValue("random");
  459. if (string.IsNullOrEmpty(ticket)) ticket = urlParameters.GetValue("ticket");
  460. if (string.IsNullOrEmpty(session)) session = urlParameters.GetValue("session");
  461. if (string.IsNullOrEmpty(page)) page = urlParameters.GetValue("page");
  462. // 从 Cookie 中获取 Ticket。
  463. var cookies = request.Cookies;
  464. if (string.IsNullOrEmpty(ticket)) ticket = cookies.GetValue("ticket");
  465. // 最后检查 URL 路径。
  466. var paths = (request.Url.AbsolutePath ?? "").Split('/');
  467. if (string.IsNullOrEmpty(application) && paths.Length >= 2) application = TextUtility.DecodeUrl(paths[1]);
  468. if (string.IsNullOrEmpty(function) && paths.Length >= 3) function = TextUtility.DecodeUrl(paths[2]);
  469. // 修正内容。
  470. application = TextUtility.Trim(application);
  471. function = TextUtility.Trim(function);
  472. random = TextUtility.Trim(random);
  473. ticket = TextUtility.Trim(ticket);
  474. session = TextUtility.Trim(session);
  475. page = TextUtility.Trim(page);
  476. // 设置请求:回传。
  477. request.Application = application;
  478. request.Function = function;
  479. request.Random = random;
  480. // 设置请求:不回传。
  481. request.Ticket = ticket;
  482. request.Session = session;
  483. request.Page = page;
  484. return request;
  485. }
  486. static StringPairs PrepareHeaders(ApiOptions options, ApiResponse response, ApiRequest request = null)
  487. {
  488. var merged = new StringPairs();
  489. if (options != null)
  490. {
  491. // 跨域访问。
  492. if (options.WithAccessControl)
  493. {
  494. merged.Add("Access-Control-Allow-Headers", "Content-Type");
  495. merged.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  496. merged.Add("Access-Control-Allow-Origin", "*");
  497. var maxage = options.AccessControlMaxAge;
  498. if (maxage > 0) merged.Add("Access-Control-Max-Age", maxage.ToString());
  499. if (request != null && request.Headers != null)
  500. {
  501. var @private = request.Headers.GetValue("Access-Control-Request-Private-Network");
  502. if (NumberUtility.Boolean(@private)) merged.Add("Access-Control-Allow-Private-Network", "true");
  503. }
  504. }
  505. // Content-Type 检查。
  506. if (options.WithContentTypeOptions || options.Default != null)
  507. {
  508. merged.Add("X-Content-Type-Options", "nosniff");
  509. }
  510. // 用于客户端,当前页面使用 HTTPS 时,将资源升级为 HTTPS。
  511. if (options.UpgradeHttps)
  512. {
  513. merged.Add("Content-Security-Policy", "upgrade-insecure-requests");
  514. }
  515. // 包含 API 的处理时间。
  516. if (options.WithDuration && response != null)
  517. {
  518. if (response.Duration.NotEmpty()) merged.Add("Duration", response.Duration);
  519. }
  520. }
  521. if (response != null)
  522. {
  523. // Cookies。
  524. var setCookies = SetCookie(response.Cookies);
  525. if (setCookies != null)
  526. {
  527. foreach (var value in setCookies) merged.Add("Set-Cookie", value);
  528. }
  529. // 自定义头。
  530. var headers = response.Headers;
  531. if (headers != null)
  532. {
  533. foreach (var header in headers)
  534. {
  535. var key = TextUtility.Trim(header.Name);
  536. if (string.IsNullOrEmpty(key)) continue;
  537. var value = header.Value;
  538. if (string.IsNullOrEmpty(value)) continue;
  539. merged.Add(key, value);
  540. }
  541. }
  542. }
  543. return merged;
  544. }
  545. internal void Output(ApiProvider provider, ApiOptions options, ApiResponse response, string type, byte[] bytes)
  546. {
  547. var preWrite = provider.PreWrite();
  548. if (!string.IsNullOrEmpty(preWrite)) return;
  549. if (response != null)
  550. {
  551. var responsePreOutput = response.PreOutput;
  552. if (responsePreOutput != null)
  553. {
  554. var @continue = responsePreOutput.Invoke(_context);
  555. if (!@continue) return;
  556. }
  557. }
  558. var invokerPreOutput = _context.Invoker.PreOutput;
  559. if (invokerPreOutput != null)
  560. {
  561. var @continue = invokerPreOutput.Invoke(_context);
  562. if (!@continue) return;
  563. }
  564. var optionsPreOutput = _context.Options.PreOutput;
  565. if (optionsPreOutput != null)
  566. {
  567. var @continue = optionsPreOutput.Invoke(_context);
  568. if (!@continue) return;
  569. }
  570. var headers = PrepareHeaders(options, response);
  571. foreach (var header in headers) provider.SetHeader(header.Key, header.Value);
  572. provider.SetCache(0);
  573. provider.SetContentType(string.IsNullOrEmpty(type) ? "application/octet-stream" : type);
  574. var length = bytes == null ? 0 : bytes.Length;
  575. provider.SetContentLength(length);
  576. if (length > 0) provider.ResponseBody().Write(bytes, 0, bytes.Length);
  577. provider.Sent();
  578. }
  579. internal void Output(ApiProvider provider, ApiOptions options, ApiResponse response, ApiRequest request, HttpMethod method)
  580. {
  581. var preWrite = provider.PreWrite();
  582. if (!string.IsNullOrEmpty(preWrite)) return;
  583. if (response != null)
  584. {
  585. var responsePreOutput = response.PreOutput;
  586. if (responsePreOutput != null)
  587. {
  588. var @continue = responsePreOutput.Invoke(_context);
  589. if (!@continue) return;
  590. }
  591. }
  592. var invokerPreOutput = _context.Invoker.PreOutput;
  593. if (invokerPreOutput != null)
  594. {
  595. var @continue = invokerPreOutput.Invoke(_context);
  596. if (!@continue) return;
  597. }
  598. var optionsPreOutput = _context.Options.PreOutput;
  599. if (optionsPreOutput != null)
  600. {
  601. var @continue = optionsPreOutput.Invoke(_context);
  602. if (!@continue) return;
  603. }
  604. // 设置头。
  605. var headers = PrepareHeaders(options, response, request);
  606. foreach (var header in headers) provider.SetHeader(header.Key, header.Value);
  607. // 自定义模型
  608. var model = response.Model as IApiModel;
  609. var result = response.Model as IActionResult;
  610. if (model != null)
  611. {
  612. try
  613. {
  614. model.Output(_context);
  615. }
  616. catch (Exception ex)
  617. {
  618. Logger.Internals.Exception(ex, model);
  619. }
  620. RuntimeUtility.Dispose(model);
  621. return;
  622. }
  623. else if (result != null)
  624. {
  625. try
  626. {
  627. result.ExecuteResult(_context);
  628. }
  629. catch (Exception ex)
  630. {
  631. Logger.Internals.Exception(ex, result);
  632. }
  633. RuntimeUtility.Dispose(result);
  634. return;
  635. }
  636. var text = ApiUtility.ToJson(response, options);
  637. var bytes = TextUtility.Bytes(text);
  638. provider.SetCache(0);
  639. provider.SetContentType("text/json; charset=utf-8");
  640. provider.SetContentLength(bytes.Length);
  641. var stream = provider.ResponseBody();
  642. if (stream != null && stream.CanWrite) stream.Write(bytes, 0, bytes.Length);
  643. provider.Sent();
  644. }
  645. #endregion
  646. }
  647. }