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