-
Notifications
You must be signed in to change notification settings - Fork 34
/
Copy pathExecutor.cs
330 lines (291 loc) · 15.1 KB
/
Executor.cs
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
using Imageflow.Fluent;
using Imageflow.Server.Configuration.Parsing;
using Imageflow.Server.HybridCache;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.DependencyInjection;
#nullable enable
namespace Imageflow.Server.Configuration.Execution;
internal record ExecutorContext(string SourcePath, Func<string,string, string> InterpolateString, IConfigRedactor Redactor, IAbstractFileMethods Fs);
internal class Executor : IAppConfigurator{
//ImageflowConfig
public Executor(ImageflowConfig config, ExecutorContext context){
this.Context = context;
this.config = config;
this.sourcePath = context.SourcePath;
}
public ExecutorContext Context { get; }
public bool RestartWhenThisFileChanges => config.AspnetServer?.RestartWhenThisFileChanges ?? false;
private readonly ImageflowConfig config;
private readonly string sourcePath;
private string InterpolateString(string input, string fieldName){
return Context.InterpolateString(input, fieldName);
}
public ImageflowMiddlewareOptions GetImageflowMiddlewareOptions(){
var options = new ImageflowMiddlewareOptions();
var routeDefaults = config.RouteDefaults;
if (config.Routes != null){
// Map physical path routes
foreach (var route in config.Routes){
//throw if from or to is null or whitespace
if (string.IsNullOrWhiteSpace(route.Prefix)){
throw new ArgumentNullException($"route.prefix is missing. Defined in file '{sourcePath}'");
}
if (string.IsNullOrWhiteSpace(route.MapToPhysicalFolder)){
throw new ArgumentNullException($"route.map_to_physical_folder is missing (other route types not yet supported). Defined in file '{sourcePath}'");
}
var prefixCaseSensitive = route.PrefixCaseSensitive ?? routeDefaults?.PrefixCaseSensitive ?? true;
var from = InterpolateString(route.Prefix, "route.prefix");
var to = InterpolateString(route.MapToPhysicalFolder, "route.map_to_physical_folder");
if (!Context.Fs.DirectoryExists(to)){
throw new DirectoryNotFoundException($"Folder '{to}' does not exist. Cannot route '{from}' to a non-existent folder. Create folder or modify [[route]] prefix='{to}' from='fix this' in '{sourcePath}' ");
}
options.MapPath(from, to, prefixCaseSensitive);
}
}
// TODO: map physical paths
// foreach(var map in config.MapPhysicalPath){
// }
// set license key
EnforceLicenseWith enforcementMethod;
// parse enforcement method string: http_402_error http_422_error watermark (default watermark)
// throw if invalid
if (string.Equals(config.License?.Enforcement, "watermark")){
enforcementMethod = EnforceLicenseWith.RedDotWatermark;
} else if (string.Equals(config.License?.Enforcement, "http_402_error")){
enforcementMethod = EnforceLicenseWith.Http402Error;
} else if (string.Equals(config.License?.Enforcement, "http_422_error")){
enforcementMethod = EnforceLicenseWith.Http422Error;
} else {
throw new FormatException($"Invalid [license] enforcement= method '{config.License?.Enforcement}'. Valid values are 'watermark', 'http_402_error', and 'http_422_error'. Defined in file '{sourcePath}'");
}
if (config.License?.Key != null){
options.SetLicenseKey(enforcementMethod, config.License.Key);
}
// set cache control
if (config.RouteDefaults?.CacheControl != null){
options.SetDefaultCacheControlString(config.RouteDefaults?.CacheControl);
}
// set diagnostics page access
var access = AccessDiagnosticsFrom.None;
if (config.Diagnostics?.AllowLocalhost ?? false){
access = AccessDiagnosticsFrom.LocalHost;
}
if (config.Diagnostics?.AllowAnyhost ?? false){
access = AccessDiagnosticsFrom.AnyHost;
}
options.SetDiagnosticsPageAccess(access);
// set diagnostics page password
if (!string.IsNullOrWhiteSpace(config.Diagnostics?.AllowWithPassword)){
options.SetDiagnosticsPagePassword(config.Diagnostics.AllowWithPassword);
}
// set hybrid cache
if (config.DiskCache?.Enabled ?? false){
options.SetAllowCaching(true);
}
// default commands
if (config.RouteDefaults?.ApplyDefaultCommands != null){
// parse as querystring using ASP.NET.
var querystring = '?' + config.RouteDefaults.ApplyDefaultCommands.TrimStart('?');
var parsed = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(querystring);
foreach(var command in parsed){
var key = command.Key;
var value = command.Value;
if (string.IsNullOrWhiteSpace(key)){
throw new ArgumentNullException($"route_defaults.apply_default_commands.key is missing. Defined in file '{sourcePath}'");
}
if (string.IsNullOrWhiteSpace(value)){
throw new ArgumentNullException($"route_defaults.apply_default_commands.value is missing. Defined in file '{sourcePath}' for key route_defaults.apply_default_commands.key '{key}'");
}
options.AddCommandDefault(key, value);
}
}
if (config.RouteDefaults?.ApplyDefaultCommandsToQuerylessUrls ?? false){
options.SetApplyDefaultCommandsToQuerylessUrls(true);
}
// set security options
var securityOptions = new SecurityOptions();
if (config.Security?.MaxDecodeResolution != null){
securityOptions.SetMaxDecodeSize(config.Security.MaxDecodeResolution.ToFrameSizeLimit());
}
if (config.Security?.MaxEncodeResolution != null){
securityOptions.SetMaxEncodeSize(config.Security.MaxEncodeResolution.ToFrameSizeLimit());
}
if (config.Security?.MaxFrameResolution != null){
securityOptions.SetMaxFrameSize(config.Security.MaxFrameResolution.ToFrameSizeLimit());
}
options.SetJobSecurityOptions(securityOptions);
return options;
}
public RewriteOptions GetRewriteOptions(){
var options = new RewriteOptions();
var apache = config.AspnetServer?.ApacheModRewrite;
// if (apache?.Text != null){
// options.AddApacheModRewrite(new StringReader(PreprocessApacheRewrites(apache.Text)));
// }
if (apache?.File != null){
// read from string, throw exception if missing, and preprocess
var path = InterpolateString(apache.File, "asp_net_server.apache_mod_rewrite.file");
if (!Context.Fs.FileExists(path)){
throw new FileNotFoundException($"Apache mod_rewrite file '{path}' does not exist. Defined in key asp_net_server.apache_mod_rewrite.file in file '{sourcePath}'");
}
var text = Context.Fs.ReadAllText(path);
options.AddApacheModRewrite(new StringReader(PreprocessApacheRewrites(text)));
}
// var iis = config.AspNetServer?.IisUrlRewrite;
// if (iis?.Text != null){
// options.AddIISUrlRewrite(new StringReader(iis.Text));
// }
// if (iis?.File != null){
// // read from string, throw exception if missing
// var path = this.varsParser.InterpolateString(iis.File, "asp_net_server.iis_url_rewrite.file");
// if (!Context.Fs.FileExists(path)){
// throw new FileNotFoundException($"IIS URL rewrite file '{path}' does not exist. Defined in key asp_net_server.iis_url_rewrite.file in file '{sourcePath}'");
// }
// var text = Context.Fs.ReadAllText(path);
// options.AddIISUrlRewrite(new StringReader(text));
// }
return options;
}
internal class ServerConfigurationOptions{
public bool UseDeveloperExceptionPage { get; set; }
public string? UseExceptionHandler { get; set; } = null;
public bool UseHsts { get; set; }
public bool UseHttpsRedirection { get; set; }
public bool UseRewriter { get; set; }
public bool UseRouting { get; set; }
internal List<StaticResponse>? Endpoints { get; set; }
// From ImageflowConfig
internal ServerConfigurationOptions(ImageflowConfig config){
UseDeveloperExceptionPage = config.AspnetServer?.UseDeveloperExceptionPage ?? false;
UseExceptionHandler = config.AspnetServer?.UseExceptionHandler ?? null;
UseHsts = config.AspnetServer?.UseHsts ?? false;
UseHttpsRedirection = config.AspnetServer?.UseHttpsRedirection ?? false;
UseRewriter = config.AspnetServer?.ApacheModRewrite?.File != null;
// config.AspNetServer?.ApacheModRewrite?.File != null ||
// UseRewriter = config.AspNetServer?.ApacheModRewrite?.Text != null ||
// config.AspNetServer?.ApacheModRewrite?.File != null ||
// config.AspNetServer?.IisUrlRewrite?.Text != null ||
// config.AspNetServer?.IisUrlRewrite?.File != null;
UseRouting = (config.StaticResponse?.Count ?? 0) > 0;
Endpoints = config.StaticResponse;
}
}
public ServerConfigurationOptions GetServerConfigurationOptions(){
return new ServerConfigurationOptions(config);
}
// Because the msft implemention is maliciously incompetent
internal static string PreprocessApacheRewrites(string text) => text.Replace("\\w", "[A-Za-z0-9_]").Replace("\\d", "[0-9]");
public HybridCacheOptions GetHybridCacheOptions(){
if (config.DiskCache == null){
throw new InvalidOperationException("Cannot call GetHybridCacheOptions() when config.DiskCache is null.");
}
var expandedFolder = config.DiskCache.Folder != null ?
InterpolateString(config.DiskCache.Folder, "disk_cache.folder") : null;
if (expandedFolder == null){
throw new InvalidOperationException("Cannot call GetHybridCacheOptions() when config.DiskCache.Folder is null.");
}
// require path exists
if (!Context.Fs.DirectoryExists(expandedFolder)){
var parent = Path.GetDirectoryName(expandedFolder);
// or at least the parent folder
if (parent == null || !Context.Fs.DirectoryExists(parent)){
throw new DirectoryNotFoundException($"Hybrid cache folder '{expandedFolder}' and its parent do not exist. Defined in file '{sourcePath}'");
}
}
var options = new HybridCacheOptions(expandedFolder);
var CacheSizeMb = config.DiskCache.CacheSizeMb ?? 0;
if (CacheSizeMb > 0){
options.CacheSizeMb = CacheSizeMb;
}
var DatabaseShards = config.DiskCache.DatabaseShards ?? 0;
if (DatabaseShards > 0){
options.DatabaseShards = DatabaseShards;
}
var writeQueueRamMb = config.DiskCache.WriteQueueRamMb ?? 0;
if (writeQueueRamMb > 0){
options.WriteQueueMemoryMb = writeQueueRamMb;
}
var EvictionSweepSizeMb = config.DiskCache.EvictionSweepSizeMb ?? 0;
if (EvictionSweepSizeMb > 0){
options.EvictionSweepSizeMb = EvictionSweepSizeMb;
}
var SecondsUntilEvictable = config.DiskCache.SecondsUntilEvictable ?? 0;
if (SecondsUntilEvictable > 0){
options.MinAgeToDelete = TimeSpan.FromSeconds(SecondsUntilEvictable);
}
return options;
}
public void ConfigureServices(IServiceCollection services){
// Unlike ImageResizer, this MUST NOT be within the application directory.
if (config.DiskCache?.Enabled ?? false){
services.AddImageflowHybridCache(GetHybridCacheOptions());
}
}
public void ConfigureApp(IApplicationBuilder app, IWebHostEnvironment env){
var options = GetServerConfigurationOptions();
if (options.UseRewriter){
app.UseRewriter(GetRewriteOptions());
}
if (options.UseDeveloperExceptionPage){
app.UseDeveloperExceptionPage();
}
if (options.UseExceptionHandler != null){
app.UseExceptionHandler(options.UseExceptionHandler);
}
if (options.UseHsts){
app.UseHsts();
}
if (options.UseHttpsRedirection){
app.UseHttpsRedirection();
}
app.UseImageflow(GetImageflowMiddlewareOptions());
if (options.UseRouting){
app.UseRouting();
}
var staticRoutes = options.Endpoints ?? new List<StaticResponse>();
if (staticRoutes.Count > 0){
app.UseEndpoints(endpoints =>
{
foreach(var route in staticRoutes){
endpoints.MapGet(route.For, async context =>
{
// validate content type and status code
context.Response.ContentType = route.ContentType ?? "text/plain";
context.Response.StatusCode = route.StatusCode ?? 200;
// if route.File is null, route.Content must be non-null
if (route.File != null){
var expandedFile = InterpolateString(route.File, "static_response.file");
await context.Response.SendFileAsync(expandedFile);
return;
}else {
if (route.Content == null){
throw new InvalidOperationException("Both route.File and route.Content are null.");
}else{
await context.Response.WriteAsync(route.Content);
}
}
});
}
});
}
}
public Dictionary<string, string> GetComputedConfiguration(bool redactSecrets)
{
var d = new Dictionary<string, string>();
Utilities.Utilities.AddToDictionaryRecursive(GetImageflowMiddlewareOptions(), d, "ImageflowMiddlewareOptions");
if (config.DiskCache?.Enabled == true){
Utilities.Utilities.AddToDictionaryRecursive(GetHybridCacheOptions(), d, "HybridCacheOptions");
}
Utilities.Utilities.AddToDictionaryRecursive(GetServerConfigurationOptions(), d, "ServerConfigurationOptions");
Utilities.Utilities.AddToDictionaryRecursive(RestartWhenThisFileChanges, d, "RestartWhenThisFileChanges");
if (redactSecrets){
foreach(var kvp in d){
d[kvp.Key] = Context.Redactor.Redact(kvp.Value);
}
}
return d;
}
}