Skip to content

Commit 63c20fb

Browse files
committed
Add preview packages Imageflow.Server.Configuration and Imageflow.Server.Host
1 parent 1be91f1 commit 63c20fb

27 files changed

+4517
-199
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
using Imageflow.Fluent;
2+
using Imageflow.Server.Configuration.Parsing;
3+
using Imageflow.Server.HybridCache;
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Rewrite;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
#nullable enable
11+
12+
namespace Imageflow.Server.Configuration.Execution;
13+
14+
15+
internal record ExecutorContext(string SourcePath, Func<string,string, string> InterpolateString, IConfigRedactor Redactor, IAbstractFileMethods Fs);
16+
internal class Executor : IAppConfigurator{
17+
//ImageflowConfig
18+
19+
public Executor(ImageflowConfig config, ExecutorContext context){
20+
this.Context = context;
21+
this.config = config;
22+
this.sourcePath = context.SourcePath;
23+
}
24+
25+
public ExecutorContext Context { get; }
26+
private readonly ImageflowConfig config;
27+
private readonly string sourcePath;
28+
29+
private string InterpolateString(string input, string fieldName){
30+
return Context.InterpolateString(input, fieldName);
31+
}
32+
33+
34+
public ImageflowMiddlewareOptions GetImageflowMiddlewareOptions(){
35+
var options = new ImageflowMiddlewareOptions();
36+
37+
38+
var routeDefaults = config.RouteDefaults;
39+
40+
41+
if (config.Routes != null){
42+
// Map physical path routes
43+
foreach (var route in config.Routes){
44+
//throw if from or to is null or whitespace
45+
if (string.IsNullOrWhiteSpace(route.Prefix)){
46+
throw new ArgumentNullException($"route.prefix is missing. Defined in file '{sourcePath}'");
47+
}
48+
if (string.IsNullOrWhiteSpace(route.MapToPhysicalFolder)){
49+
throw new ArgumentNullException($"route.map_to_physical_folder is missing (other route types not yet supported). Defined in file '{sourcePath}'");
50+
}
51+
var prefixCaseSensitive = route.PrefixCaseSensitive ?? routeDefaults?.PrefixCaseSensitive ?? true;
52+
var from = InterpolateString(route.Prefix, "route.prefix");
53+
var to = InterpolateString(route.MapToPhysicalFolder, "route.map_to_physical_folder");
54+
if (!Context.Fs.DirectoryExists(to)){
55+
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}' ");
56+
}
57+
options.MapPath(from, to, prefixCaseSensitive);
58+
}
59+
}
60+
// TODO: map physical paths
61+
// foreach(var map in config.MapPhysicalPath){
62+
63+
// }
64+
// set license key
65+
EnforceLicenseWith enforcementMethod;
66+
// parse enforcement method string: http_402_error http_422_error watermark (default watermark)
67+
// throw if invalid
68+
if (string.Equals(config.License?.Enforcement, "watermark")){
69+
enforcementMethod = EnforceLicenseWith.RedDotWatermark;
70+
} else if (string.Equals(config.License?.Enforcement, "http_402_error")){
71+
enforcementMethod = EnforceLicenseWith.Http402Error;
72+
} else if (string.Equals(config.License?.Enforcement, "http_422_error")){
73+
enforcementMethod = EnforceLicenseWith.Http422Error;
74+
} else {
75+
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}'");
76+
}
77+
if (config.License?.Key != null){
78+
options.SetLicenseKey(enforcementMethod, config.License.Key);
79+
}
80+
81+
// set cache control
82+
if (config.RouteDefaults?.CacheControl != null){
83+
options.SetDefaultCacheControlString(config.RouteDefaults?.CacheControl);
84+
}
85+
86+
// set diagnostics page access
87+
var access = AccessDiagnosticsFrom.None;
88+
if (config.Diagnostics?.AllowLocalhost ?? false){
89+
access = AccessDiagnosticsFrom.LocalHost;
90+
}
91+
if (config.Diagnostics?.AllowAnyhost ?? false){
92+
access = AccessDiagnosticsFrom.AnyHost;
93+
}
94+
options.SetDiagnosticsPageAccess(access);
95+
// set diagnostics page password
96+
if (!string.IsNullOrWhiteSpace(config.Diagnostics?.AllowWithPassword)){
97+
options.SetDiagnosticsPagePassword(config.Diagnostics.AllowWithPassword);
98+
}
99+
// set hybrid cache
100+
if (config.DiskCache?.Enabled ?? false){
101+
options.SetAllowCaching(true);
102+
}
103+
// default commands
104+
if (config.RouteDefaults?.ApplyDefaultCommands != null){
105+
// parse as querystring using ASP.NET.
106+
var querystring = '?' + config.RouteDefaults.ApplyDefaultCommands.TrimStart('?');
107+
var parsed = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(querystring);
108+
foreach(var command in parsed){
109+
var key = command.Key;
110+
var value = command.Value;
111+
if (string.IsNullOrWhiteSpace(key)){
112+
throw new ArgumentNullException($"route_defaults.apply_default_commands.key is missing. Defined in file '{sourcePath}'");
113+
}
114+
if (string.IsNullOrWhiteSpace(value)){
115+
throw new ArgumentNullException($"route_defaults.apply_default_commands.value is missing. Defined in file '{sourcePath}' for key route_defaults.apply_default_commands.key '{key}'");
116+
}
117+
options.AddCommandDefault(key, value);
118+
}
119+
120+
}
121+
122+
// set security options
123+
var securityOptions = new SecurityOptions();
124+
if (config.Security?.MaxDecodeResolution != null){
125+
securityOptions.SetMaxDecodeSize(config.Security.MaxDecodeResolution.ToFrameSizeLimit());
126+
}
127+
if (config.Security?.MaxEncodeResolution != null){
128+
securityOptions.SetMaxEncodeSize(config.Security.MaxEncodeResolution.ToFrameSizeLimit());
129+
}
130+
if (config.Security?.MaxFrameResolution != null){
131+
securityOptions.SetMaxFrameSize(config.Security.MaxFrameResolution.ToFrameSizeLimit());
132+
}
133+
options.SetJobSecurityOptions(securityOptions);
134+
return options;
135+
}
136+
public RewriteOptions GetRewriteOptions(){
137+
var options = new RewriteOptions();
138+
var apache = config.AspnetServer?.ApacheModRewrite;
139+
// if (apache?.Text != null){
140+
// options.AddApacheModRewrite(new StringReader(PreprocessApacheRewrites(apache.Text)));
141+
// }
142+
if (apache?.File != null){
143+
// read from string, throw exception if missing, and preprocess
144+
var path = InterpolateString(apache.File, "asp_net_server.apache_mod_rewrite.file");
145+
if (!Context.Fs.FileExists(path)){
146+
throw new FileNotFoundException($"Apache mod_rewrite file '{path}' does not exist. Defined in key asp_net_server.apache_mod_rewrite.file in file '{sourcePath}'");
147+
}
148+
var text = Context.Fs.ReadAllText(path);
149+
options.AddApacheModRewrite(new StringReader(PreprocessApacheRewrites(text)));
150+
}
151+
152+
// var iis = config.AspNetServer?.IisUrlRewrite;
153+
// if (iis?.Text != null){
154+
// options.AddIISUrlRewrite(new StringReader(iis.Text));
155+
// }
156+
// if (iis?.File != null){
157+
// // read from string, throw exception if missing
158+
// var path = this.varsParser.InterpolateString(iis.File, "asp_net_server.iis_url_rewrite.file");
159+
// if (!Context.Fs.FileExists(path)){
160+
// throw new FileNotFoundException($"IIS URL rewrite file '{path}' does not exist. Defined in key asp_net_server.iis_url_rewrite.file in file '{sourcePath}'");
161+
// }
162+
// var text = Context.Fs.ReadAllText(path);
163+
// options.AddIISUrlRewrite(new StringReader(text));
164+
// }
165+
166+
return options;
167+
}
168+
169+
internal class ServerConfigurationOptions{
170+
171+
public bool UseDeveloperExceptionPage { get; set; } = false;
172+
public string? UseExceptionHandler { get; set; } = null;
173+
public bool UseHsts { get; set; } = true;
174+
public bool UseHttpsRedirection { get; set; } = false;
175+
public bool UseRewriter { get; set; } = true;
176+
public bool UseRouting { get; set; } = true;
177+
internal List<StaticResponse>? Endpoints { get; set; }
178+
179+
// From ImageflowConfig
180+
internal ServerConfigurationOptions(ImageflowConfig config){
181+
UseDeveloperExceptionPage = config.AspnetServer?.UseDeveloperExceptionPage ?? false;
182+
UseExceptionHandler = config.AspnetServer?.UseExceptionHandler ?? null;
183+
UseHsts = config.AspnetServer?.UseHsts ?? false;
184+
UseHttpsRedirection = config.AspnetServer?.UseHttpsRedirection ?? false;
185+
UseRewriter = config.AspnetServer?.ApacheModRewrite?.File != null;
186+
// config.AspNetServer?.ApacheModRewrite?.File != null ||
187+
// UseRewriter = config.AspNetServer?.ApacheModRewrite?.Text != null ||
188+
// config.AspNetServer?.ApacheModRewrite?.File != null ||
189+
// config.AspNetServer?.IisUrlRewrite?.Text != null ||
190+
// config.AspNetServer?.IisUrlRewrite?.File != null;
191+
192+
UseRouting = (config.StaticResponse?.Count ?? 0) > 0;
193+
Endpoints = config.StaticResponse;
194+
}
195+
196+
}
197+
public ServerConfigurationOptions GetServerConfigurationOptions(){
198+
return new ServerConfigurationOptions(config);
199+
}
200+
201+
// Because the msft implemention is maliciously incompetent
202+
internal static string PreprocessApacheRewrites(string text) => text.Replace("\\w", "[A-Za-z0-9_]").Replace("\\d", "[0-9]");
203+
204+
205+
206+
public HybridCacheOptions GetHybridCacheOptions(){
207+
if (config.DiskCache == null){
208+
throw new InvalidOperationException("Cannot call GetHybridCacheOptions() when config.DiskCache is null.");
209+
}
210+
var expandedFolder = config.DiskCache.Folder != null ?
211+
InterpolateString(config.DiskCache.Folder, "disk_cache.folder") : null;
212+
if (expandedFolder == null){
213+
throw new InvalidOperationException("Cannot call GetHybridCacheOptions() when config.DiskCache.Folder is null.");
214+
}
215+
// require path exists
216+
if (!Context.Fs.DirectoryExists(expandedFolder)){
217+
var parent = Path.GetDirectoryName(expandedFolder);
218+
// or at least the parent folder
219+
if (parent == null || !Context.Fs.DirectoryExists(parent)){
220+
throw new DirectoryNotFoundException($"Hybrid cache folder '{expandedFolder}' and its parent do not exist. Defined in file '{sourcePath}'");
221+
}
222+
}
223+
224+
var options = new HybridCacheOptions(expandedFolder);
225+
var CacheSizeMb = config.DiskCache.CacheSizeMb ?? 0;
226+
if (CacheSizeMb > 0){
227+
options.CacheSizeMb = CacheSizeMb;
228+
}
229+
var DatabaseShards = config.DiskCache.DatabaseShards ?? 0;
230+
if (DatabaseShards > 0){
231+
options.DatabaseShards = DatabaseShards;
232+
}
233+
var writeQueueRamMb = config.DiskCache.WriteQueueRamMb ?? 0;
234+
if (writeQueueRamMb > 0){
235+
options.WriteQueueMemoryMb = writeQueueRamMb;
236+
}
237+
var EvictionSweepSizeMb = config.DiskCache.EvictionSweepSizeMb ?? 0;
238+
if (EvictionSweepSizeMb > 0){
239+
options.EvictionSweepSizeMb = EvictionSweepSizeMb;
240+
}
241+
242+
// var SecondsUntilEvictable = config.DiskCache.SecondsUntilEvictable ?? 0;
243+
// if (SecondsUntilEvictable > 0){
244+
// options.
245+
// }
246+
247+
return options;
248+
}
249+
public void ConfigureServices(IServiceCollection services){
250+
// Unlike ImageResizer, this MUST NOT be within the application directory.
251+
if (config.DiskCache?.Enabled ?? false){
252+
services.AddImageflowHybridCache(GetHybridCacheOptions());
253+
}
254+
}
255+
public void ConfigureApp(IApplicationBuilder app, IWebHostEnvironment env){
256+
var options = GetServerConfigurationOptions();
257+
if (options.UseRewriter){
258+
app.UseRewriter(GetRewriteOptions());
259+
}
260+
if (options.UseDeveloperExceptionPage){
261+
app.UseDeveloperExceptionPage();
262+
}
263+
if (options.UseExceptionHandler != null){
264+
app.UseExceptionHandler(options.UseExceptionHandler);
265+
}
266+
if (options.UseHsts){
267+
app.UseHsts();
268+
}
269+
if (options.UseHttpsRedirection){
270+
app.UseHttpsRedirection();
271+
}
272+
app.UseImageflow(GetImageflowMiddlewareOptions());
273+
274+
if (options.UseRouting){
275+
app.UseRouting();
276+
}
277+
278+
var staticRoutes = options.Endpoints ?? new List<StaticResponse>();
279+
if (staticRoutes.Count > 0){
280+
app.UseEndpoints(endpoints =>
281+
{
282+
foreach(var route in staticRoutes){
283+
endpoints.MapGet(route.For, async context =>
284+
{
285+
// validate content type and status code
286+
context.Response.ContentType = route.ContentType ?? "text/plain";
287+
context.Response.StatusCode = route.StatusCode ?? 200;
288+
// if route.File is null, route.Content must be non-null
289+
if (route.File != null){
290+
var expandedFile = InterpolateString(route.File, "static_response.file");
291+
await context.Response.SendFileAsync(expandedFile);
292+
return;
293+
}else {
294+
if (route.Content == null){
295+
throw new InvalidOperationException("Both route.File and route.Content are null.");
296+
}else{
297+
await context.Response.WriteAsync(route.Content);
298+
}
299+
}
300+
301+
});
302+
}
303+
});
304+
}
305+
306+
}
307+
308+
public Dictionary<string, string> GetComputedConfiguration(bool redactSecrets)
309+
{
310+
var d = new Dictionary<string, string>();
311+
Utilities.Utilities.AddToDictionaryRecursive(GetImageflowMiddlewareOptions(), d, "ImageflowMiddlewareOptions");
312+
if (config.DiskCache?.Enabled == true){
313+
Utilities.Utilities.AddToDictionaryRecursive(GetHybridCacheOptions(), d, "HybridCacheOptions");
314+
}
315+
Utilities.Utilities.AddToDictionaryRecursive(GetServerConfigurationOptions(), d, "ServerConfigurationOptions");
316+
if (redactSecrets){
317+
foreach(var kvp in d){
318+
d[kvp.Key] = Context.Redactor.Redact(kvp.Value);
319+
}
320+
}
321+
return d;
322+
}
323+
}

0 commit comments

Comments
 (0)