HttpApiServer.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. using System;
  2. using System.IO;
  3. using System.Net;
  4. using System.Net.Sockets;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Collections;
  8. using System.Collections.Generic;
  9. using System.Reflection;
  10. using System.Linq;
  11. using System.Text.RegularExpressions;
  12. namespace System.Net.ApiService {
  13. /*
  14. * Useage:
  15. * - Create an instance with Port
  16. * - If you want to use EventHandler, api.Request += Api_Request;
  17. * private void Api_Request(object sender, RequestEventArgs e)
  18. * - If you want to use Annotations, create a function and add [Route("/index.html") above it
  19. * Multiple Routes are allowed
  20. * private void example1(RequestHeader rh, Response r)
  21. * private void example1(RequestHeader rh, Response r, Match m)
  22. * private void example1(RequestHeader rh, Response r, GroupCollection m)
  23. * private string example1(RequestHeader rh)
  24. * private object example1(RequestHeader rh)
  25. * private bool example1(RequestHeader rh)
  26. * private string example1(RequestHeader rh, Match m)
  27. * private object example1(RequestHeader rh, Match m)
  28. * private bool example1(RequestHeader rh, Match m)
  29. * private string example1(RequestHeader rh, GroupCollection m)
  30. * private object example1(RequestHeader rh, GroupCollection m)
  31. * private bool example1(RequestHeader rh, GroupCollection m)
  32. * [...]
  33. * - Return Types:
  34. * void, string, bool, object
  35. * - Parameters
  36. * RequestHeader, Response?, Match?/GroupCollection?
  37. * - Using RegEx, "." and "/" only match inside a () {} and [], so "/index.html" will link to a file instead of "(/index.html)" that would match "indexAhtml", too
  38. */
  39. public class HttpApiServer {
  40. protected TcpListener myListener;
  41. protected ushort _port;
  42. protected bool bRunning = false;
  43. protected Thread th;
  44. private List<RouteElements> _routes = new List<RouteElements>();
  45. protected HttpMethod _allowedMethods = HttpMethod.GET | HttpMethod.POST;
  46. protected string _serverName = "BlackPro_SimpleApi";
  47. // TODO: wwwDirectory entsprechend einer Hosts.ini Datei anpassen, anhand der Request.Host wird ein entsprechender wwwDirectory / FavIcon / Robots.txt verwendet, der in der Hosts.ini hinterlegt wurde
  48. // Das soll Multiple V-Hosts in zukunft erlauben
  49. protected string? _wwwDirectory = "/www";
  50. protected bool bUseWebDirectory = true;
  51. protected bool bUseCompression = false;
  52. protected bool bAllowEmptyResponse = false;
  53. protected string Path_FavIcon = "/favicon.ico";
  54. protected string Path_RobotsTXT = "/robots.txt";
  55. private string[] _allowedEncodingTypes = new string[] {
  56. "text/html",
  57. "application/javascript",
  58. "text/javascript",
  59. "application/json",
  60. "text/json",
  61. "text/css",
  62. "text/stylesheet",
  63. "text/xml",
  64. "application/xml"
  65. };
  66. public RouteElements[] Routes {
  67. get { return this._routes.ToArray(); }
  68. }
  69. public bool Running {
  70. get { return this.bRunning; }
  71. }
  72. public HttpApiServer(ushort port) {
  73. this.Port = port;
  74. bUseWebDirectory = Directory.Exists(this._wwwDirectory);
  75. }
  76. public HttpApiServer(string www_directory, ushort port) {
  77. this.Port = port;
  78. this.WebDirectory = www_directory;
  79. }
  80. public bool AllowEmptyResponse {
  81. get { return this.bAllowEmptyResponse; }
  82. set { this.bAllowEmptyResponse = value; }
  83. }
  84. public HttpMethod AllowedMethods {
  85. get {
  86. return this._allowedMethods;
  87. }
  88. set {
  89. this._allowedMethods = value;
  90. }
  91. }
  92. public bool Compression {
  93. get {
  94. return this.bUseCompression;
  95. }
  96. set {
  97. this.bUseCompression = value;
  98. }
  99. }
  100. public string ServerName {
  101. get {
  102. return this._serverName;
  103. }
  104. set {
  105. this._serverName = value;
  106. }
  107. }
  108. public bool UseWebDirectory {
  109. get {
  110. return this.bUseWebDirectory;
  111. }
  112. }
  113. public string WebDirectory {
  114. get {
  115. return this._wwwDirectory;
  116. }
  117. set {
  118. this._wwwDirectory = value;
  119. this.bUseWebDirectory = this._wwwDirectory != null && Directory.Exists(this._wwwDirectory) && this._wwwDirectory != "";
  120. }
  121. }
  122. public ushort Port {
  123. get {
  124. return this._port;
  125. }
  126. set {
  127. this._port = value;
  128. if (this.bRunning) {
  129. this.Stop();
  130. myListener = new TcpListener(this._port);
  131. this.Start();
  132. } else {
  133. myListener = new TcpListener(this._port);
  134. }
  135. //myListener.Start();
  136. }
  137. }
  138. public bool Pending {
  139. get => this.myListener.Pending();
  140. }
  141. public void Start() {
  142. this.bRunning = true;
  143. if (myListener==null) {
  144. myListener = new TcpListener(this._port);
  145. }
  146. myListener.Start();
  147. this.th = new Thread(new ThreadStart(this.StartListen));
  148. this.th.Start();
  149. }
  150. public void Stop() {
  151. try {
  152. myListener.Server.Close();
  153. myListener.Stop();
  154. myListener = null;
  155. } catch {
  156. }
  157. try {
  158. this.th.Abort();
  159. } catch {
  160. }
  161. this.bRunning = false;
  162. }
  163. /*public void SendHeader(string sHttpVersion, string sMIMEHeader, int iTotBytes, string sStatusCode, ref Socket mySocket) {
  164. String sBuffer = "";
  165. // if Mime type is not provided set default to text/html
  166. if (sMIMEHeader.Length == 0) {
  167. sMIMEHeader = "text/html";// Default Mime Type is text/html
  168. }
  169. sBuffer = sBuffer + sHttpVersion + sStatusCode + "\r\n";
  170. sBuffer = sBuffer + "Server: BPApiServer\r\n";
  171. sBuffer = sBuffer + "Content-Type: " + sMIMEHeader + "\r\n";
  172. sBuffer = sBuffer + "Accept-Ranges: bytes\r\n";
  173. sBuffer = sBuffer + "Content-Length: " + iTotBytes + "\r\n\r\n";
  174. Byte[] bSendData = Encoding.ASCII.GetBytes(sBuffer);
  175. SendToBrowser(bSendData, ref mySocket);
  176. }*/
  177. public bool SendToBrowser(String sData, ref Socket mySocket) {
  178. return SendToBrowser(Encoding.ASCII.GetBytes(sData), ref mySocket);
  179. }
  180. public bool SendToBrowser(Byte[] bSendData, ref Socket mySocket) {
  181. int numBytes = 0;
  182. try {
  183. if (mySocket.Connected) {
  184. if ((numBytes = mySocket.Send(bSendData, bSendData.Length, 0)) > 0)
  185. return true;
  186. }
  187. } catch (Exception e) {
  188. }
  189. return false;
  190. }
  191. protected void StartListen() {
  192. while (true) {
  193. //Accept a new connection
  194. Socket mySocket;
  195. try {
  196. mySocket = myListener.AcceptSocket();
  197. } catch {
  198. break;
  199. }
  200. DateTime startTime = DateTime.Now;
  201. TimeSpan timeout = new TimeSpan(0, 0, 10);
  202. if (mySocket.Connected) {
  203. Byte[] bReceive = new Byte[4096];
  204. int i = mySocket.Receive(bReceive, 0, mySocket.Available, SocketFlags.Peek);
  205. string sBuffer = Encoding.ASCII.GetString(bReceive, 0, i);
  206. string sBuffer2 = "";
  207. foreach(byte b in bReceive) {
  208. if (b > 0)
  209. sBuffer2 += b;
  210. }
  211. if(sBuffer2.Length==0) {
  212. i = mySocket.Receive(bReceive, 0, mySocket.Available, SocketFlags.Peek);
  213. sBuffer = Encoding.ASCII.GetString(bReceive,0,i);
  214. foreach (byte b in bReceive) {
  215. if (b > 0)
  216. sBuffer2 += b;
  217. }
  218. }
  219. System.Diagnostics.Debug.WriteLine("- Received[" + sBuffer2.Length + "]: '" + sBuffer2.Trim() + "'");
  220. RequestHeader header = new RequestHeader(sBuffer);
  221. Response resp = new Response();
  222. RequestEventArgs req = new RequestEventArgs();
  223. req.Response = resp;
  224. req.Request = header;
  225. req.Socket = mySocket;
  226. req.SendAnyResponse = true;
  227. if (header.Path == "/ping" || header.Path=="ping") {
  228. resp.FromString(200, "pong");
  229. } else if (req.Request.Path == "/favicon.ico") {
  230. resp.FromFile("favicon.ico");
  231. } else if (req.Request.Path == "/robots.txt") {
  232. resp.FromFile("robots.txt");
  233. } else {
  234. if ((header.Method | this._allowedMethods) != this._allowedMethods) {
  235. resp.Error(Response.eErrorCode.MethodNotAllowed);
  236. } else {
  237. RequestAttr(req);
  238. OnRequest(req);
  239. }
  240. }
  241. if (req.Response == null) {
  242. req.Response = new Response();
  243. req.Response.Error(Response.eErrorCode.InternalServerError);
  244. }
  245. if (req.Response == null) {
  246. req.Request = header;
  247. req.Response.Error(Response.eErrorCode.InternalServerError);
  248. }
  249. if (req.Response.Code < 400) {
  250. System.Diagnostics.Debug.WriteLine("Request: " + header.Path);
  251. if (!req.Response.IsSet) {
  252. System.Diagnostics.Debug.WriteLine("No response was set");
  253. if (this.bUseWebDirectory) {
  254. System.Diagnostics.Debug.WriteLine("Use www-Directory");
  255. System.Diagnostics.Debug.WriteLine("Dir: " + this._wwwDirectory);
  256. System.Diagnostics.Debug.WriteLine("File: " + req.Request.Path);
  257. FileInfo fi = new FileInfo(this._wwwDirectory + req.Request.Path);
  258. System.Diagnostics.Debug.WriteLine("Full: " + fi.FullName);
  259. if (fi.Exists) {
  260. req.Response.FromFile(fi.FullName);
  261. }
  262. }
  263. }
  264. System.Diagnostics.Debug.WriteLine("Done");
  265. }
  266. if (req.SendAnyResponse) {
  267. List<string> response_header = new List<string>();
  268. if(this.bAllowEmptyResponse && req.Response.Length==0) {
  269. SetDefaultResponse(req.Response);
  270. }
  271. response_header.Add("HTTP/1.1 " + req.Response.Code + " " + req.Response.CodeName);
  272. response_header.Add("Server: " + this._serverName);
  273. response_header.Add("Date: " + DateTime.Now.ToString("r"));
  274. bool bUseBRCompression = false;
  275. bool bUseGZCompression = false;
  276. bool bUseDeflateCompression = false;
  277. Span<byte> compressed_content = new Span<byte>();
  278. int bytesWritten;
  279. if (req.Response.Code >= 200 && req.Response.Code < 300 && resp.FilePath == "" && this.bUseCompression) {
  280. if (this._allowedEncodingTypes.Contains(req.Response.ContentType)) {
  281. if ((header.AcceptEncoding & RequestHeader.eEncoder.br) == header.AcceptEncoding) {
  282. if (System.IO.Compression.BrotliEncoder.TryCompress(resp.Content, compressed_content, out bytesWritten)) {
  283. bUseBRCompression = true;
  284. }
  285. } else if ((header.AcceptEncoding & RequestHeader.eEncoder.gzip) == header.AcceptEncoding) {
  286. using (MemoryStream mem = new MemoryStream()) {
  287. using (System.IO.Compression.GZipStream gzip = new IO.Compression.GZipStream(mem, IO.Compression.CompressionLevel.Optimal)) {
  288. gzip.Write(resp.Content, 0, resp.Content.Length);
  289. }
  290. compressed_content = mem.ToArray();
  291. }
  292. bUseGZCompression = true;
  293. } else if ((header.AcceptEncoding & RequestHeader.eEncoder.deflate) == header.AcceptEncoding) {
  294. using (MemoryStream mem = new MemoryStream()) {
  295. using (System.IO.Compression.DeflateStream defl = new IO.Compression.DeflateStream(mem, IO.Compression.CompressionLevel.Optimal)) {
  296. defl.Write(resp.Content, 0, resp.Content.Length);
  297. }
  298. compressed_content = mem.ToArray();
  299. }
  300. bUseDeflateCompression = true;
  301. }
  302. }
  303. }
  304. if (req.Response.Code >= 300 && req.Response.Code < 400 && req.Response.RedirectLocation != "") {
  305. response_header.Add("Location: " + req.Response.RedirectLocation);
  306. } else if (req.Response.Code >= 500) {
  307. // Nothing to do on 500 errors
  308. } else {
  309. if (bUseBRCompression || bUseGZCompression || bUseDeflateCompression)
  310. response_header.Add("Content-Length: " + compressed_content.Length);
  311. else
  312. response_header.Add("Content-Length: " + req.Response.Length);
  313. if(req.Response.ContentType == "") {
  314. response_header.Add("Content-Type: " + req.Response.ContentType);
  315. }
  316. response_header.Add("Accept-Ranges: bytes");
  317. response_header.Add("Age: 0");
  318. response_header.Add("Accept-Ranges: bytes");
  319. if (resp.CacheControl != null && resp.CacheControl.IsSet) {
  320. response_header.Add("Cache-Control: " + resp.CacheControl);
  321. }
  322. if (bUseBRCompression)
  323. response_header.Add("Content-Encoding: bz");
  324. else if (bUseGZCompression)
  325. response_header.Add("Content-Encoding: gzip");
  326. else if (bUseDeflateCompression)
  327. response_header.Add("Content-Encoding: deflate");
  328. response_header.Add("Connection: " + (resp.Connection == Response.eConnection.KeepAlive ? "keep-alive" : "Close"));
  329. }
  330. string response_header_string = "";
  331. foreach (string s in response_header)
  332. response_header_string += s + "\r\n";
  333. response_header_string += "\r\n";
  334. System.Diagnostics.Debug.WriteLine(response_header_string);
  335. this.SendToBrowser(Encoding.ASCII.GetBytes(response_header_string), ref mySocket);
  336. if (req.Response.FilePath != "" && req.Response.Code == 200) {
  337. System.IO.FileStream fs = new FileStream(req.Response.FilePath, FileMode.Open);
  338. BinaryReader br = new BinaryReader(fs);
  339. while (fs.Position < fs.Length) {
  340. byte[] buffer = br.ReadBytes(1000);
  341. this.SendToBrowser(buffer, ref mySocket);
  342. }
  343. fs.Close();
  344. } else {
  345. if (bUseBRCompression || bUseGZCompression || bUseDeflateCompression) {
  346. System.Diagnostics.Debug.WriteLine("Send compressed: " + compressed_content.Length);
  347. this.SendToBrowser(compressed_content.ToArray(), ref mySocket);
  348. } else {
  349. if (req.Response.Connection != null && req.Response.Length>0) {
  350. System.Diagnostics.Debug.WriteLine("Send uncompressed: " + req.Response.Content.Length);
  351. if (this.SendToBrowser(req.Response.Content, ref mySocket)) {
  352. System.Diagnostics.Debug.WriteLine("Success");
  353. } else {
  354. System.Diagnostics.Debug.WriteLine("Failed");
  355. }
  356. }
  357. }
  358. }
  359. }
  360. mySocket.Close();
  361. }
  362. }
  363. }
  364. protected void SetDefaultResponse(Response res) {
  365. switch(res.ContentType.ToLower().Trim()) {
  366. case "":
  367. case "text/html":
  368. res.FromString("<html></html>", "text/html");
  369. break;
  370. case "text/stylesheet":
  371. case "text/css":
  372. res.FromString("");
  373. break;
  374. case "text/javascript":
  375. case "application/javascript":
  376. case "text/x-javascript":
  377. case "application/x-javascript":
  378. res.FromString("");
  379. break;
  380. }
  381. }
  382. public void RegisterClass(object[] instances) {
  383. foreach (object o in instances)
  384. this.RegisterClass(o);
  385. }
  386. public void RegisterClass(Type[] t) {
  387. foreach (Type o in t)
  388. this.RegisterClass(o);
  389. }
  390. public void RegisterClass(object instance) {
  391. Type t = instance.GetType();
  392. MethodInfo[] methodInfos = t.GetMethods(BindingFlags.Public | BindingFlags.Static);
  393. Route r = t.GetCustomAttribute<Route>();
  394. string baseRoute = "";
  395. if (r != null) {
  396. baseRoute = r.Path;
  397. }
  398. foreach (MethodInfo mi in methodInfos) {
  399. Route[] ris = mi.GetCustomAttributes<Route>().ToArray();
  400. foreach (Route ri in ris) {
  401. this._routes.Add(new RouteElements(instance, mi, ri, baseRoute));
  402. }
  403. }
  404. }
  405. public void RegisterClass(Type t) {
  406. Route r = t.GetCustomAttribute<Route>();
  407. string baseRoute = "";
  408. if (r != null) {
  409. baseRoute = r.Path;
  410. }
  411. MethodInfo[] methodInfos = t.GetMethods(BindingFlags.Public | BindingFlags.Static);
  412. foreach (MethodInfo mi in methodInfos) {
  413. Route[] ris = mi.GetCustomAttributes<Route>().ToArray();
  414. foreach (Route ri in ris) {
  415. this._routes.Add(new RouteElements(t, mi, ri, baseRoute));
  416. }
  417. }
  418. }
  419. protected void RequestAttr(RequestEventArgs e) {
  420. RequestHeader rh;
  421. foreach (RouteElements re in this._routes) {
  422. rh = e.Request;
  423. bool bIsMethod = (rh.Method | re.Method) == re.Method;
  424. if (rh.Path != null && re.Pattern.IsMatch(rh.Path) && bIsMethod) {
  425. ParameterInfo[] pis = re.Function.GetParameters();
  426. if (pis.Length == 2 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(Response)) {
  427. re.Function.Invoke(re.Instance, new object[] { e.Request, e.Response });
  428. } else if (pis.Length == 1 && pis[0].ParameterType == typeof(RequestHeader)) {
  429. if (re.Function.ReturnType == typeof(Response)) {
  430. Response r = (Response)re.Function.Invoke(re.Instance, new object[] { rh });
  431. if (r != null) {
  432. e.Response = r;
  433. }
  434. } else if (re.Function.ReturnType == typeof(string)) {
  435. string r = (string)re.Function.Invoke(re.Instance, new object[] { rh });
  436. e.Response.FromString(200, r);
  437. } else if (re.Function.ReturnType == typeof(bool)) {
  438. bool r = (bool)re.Function.Invoke(re.Instance, new object[] { rh });
  439. if (r)
  440. e.Response.FromString(200, "{\"status\":\"ok\",\"code\":200,\"value\":1}", "application/json");
  441. else
  442. e.Response.FromString(200,"{\"status\":\"failed\",\"code\":200,\"value\":0}", "application/json");
  443. } else if (re.Function.ReturnType == typeof(object)) {
  444. object r = (object)re.Function.Invoke(re.Instance, new object[] { rh });
  445. e.Response.FromObject(200,r);
  446. }
  447. } else if (pis.Length == 1 && pis[0].ParameterType == typeof(Response)) {
  448. re.Function.Invoke(re.Instance, new object[] { e.Response });
  449. //RegEx
  450. } else if (pis.Length == 3 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(Response) && pis[2].ParameterType==typeof(Match)) {
  451. re.Function.Invoke(re.Instance, new object[] { e.Request, e.Response, re.Pattern.Match(rh.Path) });
  452. } else if (pis.Length == 2 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(Match)) {
  453. if (re.Function.ReturnType == typeof(Response)) {
  454. Response r = (Response)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path) });
  455. if (r != null) {
  456. e.Response = r;
  457. }
  458. } else if (re.Function.ReturnType == typeof(string)) {
  459. string r = (string)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path) });
  460. e.Response.FromString(200,r);
  461. } else if (re.Function.ReturnType == typeof(bool)) {
  462. bool r = (bool)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path) });
  463. if (r)
  464. e.Response.FromString(200,"{\"status\":\"ok\",\"code\":200,\"value\":1}", "application/json");
  465. else
  466. e.Response.FromString(200,"{\"status\":\"failed\",\"code\":200,\"value\":0}","application/json");
  467. } else if (re.Function.ReturnType == typeof(object)) {
  468. object r = (object)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path) });
  469. e.Response.FromObject(200,r);
  470. }
  471. } else if (pis.Length == 2 && pis[0].ParameterType == typeof(Response) && pis[1].ParameterType == typeof(Match)) {
  472. re.Function.Invoke(re.Instance, new object[] { e.Response, re.Pattern.Match(rh.Path) });
  473. } else if (pis.Length == 3 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(Response) && pis[2].ParameterType == typeof(GroupCollection)) {
  474. re.Function.Invoke(re.Instance, new object[] { e.Request, e.Response, re.Pattern.Match(rh.Path).Groups });
  475. } else if (pis.Length == 2 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(GroupCollection)) {
  476. if (re.Function.ReturnType == typeof(Response)) {
  477. Response r = (Response)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path).Groups });
  478. if (r != null) {
  479. e.Response = r;
  480. }
  481. } else if (re.Function.ReturnType == typeof(string)) {
  482. string r = (string)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path).Groups });
  483. e.Response.FromString(200,r);
  484. } else if (re.Function.ReturnType == typeof(bool)) {
  485. bool r = (bool)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path).Groups });
  486. if (r)
  487. e.Response.FromString(200,"{\"status\":\"ok\",\"code\":200,\"value\":1}", "application/json");
  488. else
  489. e.Response.FromString(200,"{\"status\":\"failed\",\"code\":200,\"value\":0}", "application/json");
  490. } else if (re.Function.ReturnType == typeof(object)) {
  491. object r = (object)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path).Groups });
  492. e.Response.FromObject(200,r);
  493. }
  494. } else if (pis.Length == 2 && pis[0].ParameterType == typeof(Response) && pis[1].ParameterType == typeof(GroupCollection)) {
  495. re.Function.Invoke(re.Instance, new object[] { e.Response, re.Pattern.Match(rh.Path).Groups });
  496. }
  497. }
  498. }
  499. }
  500. protected virtual void OnRequest(RequestEventArgs e) {
  501. EventHandler<RequestEventArgs> handler = Request;
  502. if (handler != null) {
  503. handler(this, e);
  504. }
  505. }
  506. public event EventHandler<RequestEventArgs> Request;
  507. }
  508. }