-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathFloatEngine.cs
More file actions
496 lines (426 loc) · 15.4 KB
/
FloatEngine.cs
File metadata and controls
496 lines (426 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
/**
* Float
*
* A simplistic C# ASP.NET route handler with dynamic URLs and middleware capabilities.
*
* @author
* Stian Hanger (pdnagilum@gmail.com)
*
* @source
* https://github.com/nagilum/float
*/
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Script.Serialization;
namespace FloatEngine {
/// <summary>
/// Main handler for the Float API route engine.
/// </summary>
public class RouteHandler {
/// <summary>
/// A list of global middleware functions.
/// </summary>
private static readonly List<Action<RouteWrapper>> globalMiddlewareFunctions = new List<Action<RouteWrapper>>();
/// <summary>
/// A list of headers to always respond with.
/// </summary>
private static readonly NameValueCollection globalResponseHeaders = new NameValueCollection();
/// <summary>
/// JSON serialize engine.
/// </summary>
private static readonly JavaScriptSerializer jss = new JavaScriptSerializer();
/// <summary>
/// All registered routes.
/// </summary>
private static readonly List<Route> routeTable = new List<Route>();
/// <summary>
/// Allowed HTTP methods for the routes.
/// </summary>
public enum HttpMethod {
CONNECT,
DELETE,
HEAD,
GET,
OPTIONS,
PATCH,
POST,
PUT,
TRACE
}
/// <summary>
/// Add a header that will be added to all responses.
/// </summary>
/// <param name="key">Key</param>
/// <param name="value">Value</param>
public static void AddGlobalHeader(string key, string value) {
globalResponseHeaders.Add(key, value);
}
/// <summary>
/// Add a middlewere function to the global list.
/// </summary>
/// <param name="function">The function to add.</param>
public static void AddGlobalMiddleware(Action<RouteWrapper> function) {
globalMiddlewareFunctions.Add(function);
}
/// <summary>
/// Handle each request to the API.
/// </summary>
/// <param name="request">The active context request.</param>
/// <param name="response">The active context response.</param>
public static void HandleRequest(HttpRequest request, HttpResponse response) {
response.Clear();
HttpMethod httpMethod;
if (!Enum.TryParse(request.HttpMethod, out httpMethod)) {
response.StatusCode = 405;
response.End();
}
// Add global headers.
foreach (string key in globalResponseHeaders)
response.Headers.Add(key, globalResponseHeaders[key]);
// Check for auto-origins.
if (Options.AutoAddAllowOrigin)
response.Headers.Add("Access-Control-Allow-Origin", Options.AccessControlAllowOrigin);
// Check for auto-reply for OPTIONS call.
if (Options.HandleApiCallForHttpMethodOptions &&
httpMethod == HttpMethod.OPTIONS) {
if (!Options.AutoAddAllowOrigin)
response.Headers.Add("Access-Control-Allow-Origin", Options.AccessControlAllowOrigin);
response.Headers.Add("Access-Control-Max-Age", string.Format("{0}", Options.AccessControlMaxAge));
response.Headers.Add("Access-Control-Allow-Headers", string.Join(", ", Options.AccessControlAllowHeaders));
response.Headers.Add("Access-Control-Allow-Methods", string.Join(", ", Options.AccessControlAllowMethods));
response.End();
}
// Ready the wrapper object.
var wrapper = new RouteWrapper {
HttpMethod = httpMethod,
RequestHeaders = request.Headers,
RequestObject = request,
RequestUrl = request.Url.AbsolutePath,
RequestUrlSections = request.Url.AbsolutePath.Substring(1).Split('/'),
ResponseHeaders = new NameValueCollection(),
ResponseObject = response,
RouteParams = new Dictionary<string, string>()
};
// Read the posted body params from the input-stream.
try {
using (var inputStream = request.InputStream) {
using (var streamReader = new StreamReader(inputStream, Encoding.UTF8)) {
wrapper.BodyParams = jss.Deserialize<Dictionary<string, string>>(streamReader.ReadToEnd());
}
}
}
catch {
wrapper.BodyParams = new Dictionary<string, string>();
}
// Run all the global middleware functions.
if (globalMiddlewareFunctions.Any()) {
foreach (var gmwf in globalMiddlewareFunctions) {
try {
gmwf(wrapper);
}
catch (FloatException ex) {
response.StatusCode = ex.HttpStatusCode ?? 500;
if (ex.Output != null) writeOutput(response, ex.Output);
response.End();
}
catch {
response.StatusCode = 500;
response.End();
}
}
}
// Find a matching route.
var routes = routeTable
.Where(r => r.RouteUrlSections.Length == wrapper.RequestUrlSections.Length &&
r.HttpMethod == wrapper.HttpMethod)
.ToList();
// Check each route for a match.
foreach (var route in routes) {
var hits = 0;
for (var i = 0; i < wrapper.RequestUrlSections.Length; i++) {
if (route.RouteUrlSections[i].StartsWith("{") &&
route.RouteUrlSections[i].EndsWith("}")) {
var key = route.RouteUrlSections[i].Substring(1, route.RouteUrlSections[i].Length - 2);
var value = wrapper.RequestUrlSections[i];
var valid = true;
if (route.ValueValidators != null) {
var regExValidator = route.ValueValidators[key];
if (!string.IsNullOrWhiteSpace(regExValidator)) {
var regex = new Regex(regExValidator);
valid = regex.IsMatch(value);
}
}
if (!valid)
break;
wrapper.RouteParams.Add(
key,
value);
hits++;
}
else if (route.RouteUrlSections[i] == wrapper.RequestUrlSections[i]) {
hits++;
}
}
if (hits != wrapper.RequestUrlSections.Length)
continue;
wrapper.RouteUrl = route.RouteUrl;
// If the accepted route has any middleware, execute it.
if (route.MiddlewareFunctions != null &&
route.MiddlewareFunctions.Any()) {
foreach (var mwf in route.MiddlewareFunctions) {
try {
mwf(wrapper);
}
catch (FloatException ex) {
response.StatusCode = ex.HttpStatusCode ?? 500;
if (ex.Output != null) writeOutput(response, ex.Output);
response.End();
}
catch {
response.StatusCode = 500;
response.End();
}
}
}
// Call the main API method and handle the response/exception.
object output = null;
response.StatusCode = 200;
try {
output = route.RouteHandlerFunction(wrapper);
}
catch (FloatException ex) {
response.StatusCode = ex.HttpStatusCode ?? 500;
if (ex.Output != null) writeOutput(response, ex.Output);
response.End();
}
catch {
response.StatusCode = 500;
response.End();
}
if (wrapper.ResponseHeaders.Count > 0)
foreach (var key in wrapper.ResponseHeaders.AllKeys)
response.Headers[key] = wrapper.ResponseHeaders[key];
if (wrapper.ResponseStatusCode.HasValue)
response.StatusCode = wrapper.ResponseStatusCode.Value;
if (output != null)
writeOutput(response, output);
response.End();
}
}
/// <summary>
/// Add a new route to the route table.
/// </summary>
/// <param name="routeUrl">Descriptive route with variables.</param>
/// <param name="httpMethod">HTTP method for route.</param>
/// <param name="routeHandler">The function to execute when this route is called.</param>
/// <param name="routeValues">A list of Regex validators for each variable in the route. (Optional)</param>
/// <param name="middlewareFunctions">A list of middleware function to call before the main call. (Optional)</param>
public static void RegisterRoute(
string routeUrl,
HttpMethod httpMethod,
Func<RouteWrapper, object> routeHandler,
Dictionary<string, string> routeValues = null,
List<Action<RouteWrapper>> middlewareFunctions = null) {
// Verify that the same route with the same HTTP method isn't already in the table.
if (routeTable.SingleOrDefault(r => r.RouteUrl == routeUrl &&
r.HttpMethod == httpMethod) != null)
throw new Exception("The same route with the same method already exist.");
// Add route to table.
routeTable.Add(
new Route {
RouteUrl = routeUrl,
RouteUrlSections = routeUrl.Split('/'),
HttpMethod = httpMethod,
RouteHandlerFunction = routeHandler,
ValueValidators = routeValues,
MiddlewareFunctions = middlewareFunctions
});
}
/// <summary>
/// Add a new route to the route table.
/// </summary>
/// <param name="routeUrl">Descriptive route with variables.</param>
/// <param name="httpMethods">A list of HTTP methods for route.</param>
/// <param name="routeHandler">The function to execute when this route is called.</param>
/// <param name="routeValues">A list of Regex validators for each variable in the route. (Optional)</param>
/// <param name="middlewareFunctions">A list of middleware function to call before the main call. (Optional)</param>
public static void RegisterRoute(
string routeUrl,
List<HttpMethod> httpMethods,
Func<RouteWrapper, object> routeHandler,
Dictionary<string, string> routeValues = null,
List<Action<RouteWrapper>> middlewareFunctions = null) {
foreach (var httpMethod in httpMethods) {
// Verify that the same route with the same HTTP method isn't already in the table.
if (routeTable.SingleOrDefault(r => r.RouteUrl == routeUrl &&
r.HttpMethod == httpMethod) != null)
throw new Exception("The same route with the same method already exist.");
// Add route to table.
routeTable.Add(
new Route {
RouteUrl = routeUrl,
RouteUrlSections = routeUrl.Split('/'),
HttpMethod = httpMethod,
RouteHandlerFunction = routeHandler,
ValueValidators = routeValues,
MiddlewareFunctions = middlewareFunctions
});
}
}
/// <summary>
/// Write given output to response stream.
/// </summary>
/// <param name="response">Response object to use.</param>
/// <param name="output">Output to write.</param>
private static void writeOutput(HttpResponse response, object output) {
response.ContentType = Options.DefaultContentType;
if (Options.SerializeToJSON)
response.Write(jss.Serialize(output));
else if (output is string)
response.Write(output);
}
/// <summary>
/// RouteTable list base class.
/// </summary>
private class Route {
/// <summary>
/// Descriptive route with variables.
/// </summary>
public string RouteUrl { get; set; }
/// <summary>
/// Section list of route.
/// </summary>
public string[] RouteUrlSections { get; set; }
/// <summary>
/// HTTP method for route.
/// </summary>
public HttpMethod HttpMethod { get; set; }
/// <summary>
/// The function to execute when this route is called.
/// </summary>
public Func<RouteWrapper, object> RouteHandlerFunction { get; set; }
/// <summary>
/// A list of Regex validators for each variable in the route.
/// </summary>
public Dictionary<string, string> ValueValidators { get; set; }
/// <summary>
/// A list of middleware function to call before the main call.
/// </summary>
public List<Action<RouteWrapper>> MiddlewareFunctions { get; set; }
}
}
/// <summary>
/// Various options for the route engine.
/// </summary>
public class Options {
/// <summary>
/// Whether or not to serialize all output to JSON before forwarding it to the client.
/// </summary>
public static bool SerializeToJSON = true;
/// <summary>
/// The default content-type header to respond to the client with.
/// </summary>
public static string DefaultContentType = "application/json; charset=utf-8";
/// <summary>
/// Whether or not to automatically answer the OPTIONS call to the framework.
/// </summary>
public static bool HandleApiCallForHttpMethodOptions = true;
/// <summary>
/// A list of headers to respond with in an OPTIONS call.
/// </summary>
public static List<RouteHandler.HttpMethod> AccessControlAllowMethods = new List<RouteHandler.HttpMethod> { RouteHandler.HttpMethod.GET, RouteHandler.HttpMethod.POST, RouteHandler.HttpMethod.DELETE, RouteHandler.HttpMethod.OPTIONS };
/// <summary>
/// The allowed origin for calls towards this API.
/// </summary>
public static string AccessControlAllowOrigin = "*";
/// <summary>
/// Max age for API options call.
/// </summary>
public static int AccessControlMaxAge = 3600;
/// <summary>
/// Headers to reply as allowed.
/// </summary>
public static List<string> AccessControlAllowHeaders = new List<string> { "Content-Type", "Authorization" };
/// <summary>
/// Whether or not to automatically add the Access-Control-Allow-Origin header to all responses.
/// </summary>
public static bool AutoAddAllowOrigin = true;
}
/// <summary>
/// HTTP status code bound exception, for terminating a function with a given status code.
/// </summary>
public class FloatException : Exception {
/// <summary>
/// The set HTTP status code.
/// </summary>
public int? HttpStatusCode { get; set; }
/// <summary>
/// Optional object to output.
/// </summary>
public object Output { get; set; }
/// <summary>
/// Init a new instance of the FloatException with a status code.
/// </summary>
/// <param name="httpStatusCode">HTTP status code to response with.</param>
/// <param name="output">Optional object to output.</param>
public FloatException(int httpStatusCode, object output = null) {
this.HttpStatusCode = httpStatusCode;
this.Output = output;
}
}
/// <summary>
/// Container for all things during the request life.
/// </summary>
public class RouteWrapper {
/// <summary>
/// The dynamic URL that matched the request URL.
/// </summary>
public string RouteUrl { get; set; }
/// <summary>
/// The actual request URL.
/// </summary>
public string RequestUrl { get; set; }
/// <summary>
/// Request URL divided into sections.
/// </summary>
public string[] RequestUrlSections { get; set; }
/// <summary>
/// The HTTP method for the request.
/// </summary>
public RouteHandler.HttpMethod HttpMethod { get; set; }
/// <summary>
/// A list of variables from the route and their value.
/// </summary>
public Dictionary<string, string> RouteParams { get; set; }
/// <summary>
/// A list of posted variables and their value.
/// </summary>
public Dictionary<string, string> BodyParams { get; set; }
/// <summary>
/// A list of all headers from the request.
/// </summary>
public NameValueCollection RequestHeaders { get; set; }
/// <summary>
/// A list of headers to be added to the response.
/// </summary>
public NameValueCollection ResponseHeaders { get; set; }
/// <summary>
/// If you wish to set a specific HTTP status code for the response.
/// </summary>
public int? ResponseStatusCode { get; set; }
/// <summary>
/// The actual ASPx request object.
/// </summary>
public HttpRequest RequestObject { get; set; }
/// <summary>
/// The actual ASPx response object.
/// </summary>
public HttpResponse ResponseObject { get; set; }
}
}