@@ -51,34 +51,46 @@ public static class StaticFilesExtensions
5151} ;
5252
5353private const string FORWARDED_PREFIX_HEADER = "X-Forwarded-Prefix" ;
54- private const string DEFAULT_FORWARDED_PREFIX = "/ngclient" ;
54+ private const string FORWARDED_PREFIX_HEADER_ALT = "X-Forwarded-Prefix-Alt" ;
55+ private const string FORWARDED_PREFIX_HEADER_NGCLIENT = "X-Forwarded-Prefix-Ngclient" ;
56+ private const string ENABLE_IFRAME_HOSTING_HEADER = "X-Allow-Iframe-Hosting" ;
57+ private const string ENABLE_IFRAME_HOSTING_ENVIRONMENT_VARIABLE = "DUPLICATI_ENABLE_IFRAME_HOSTING" ;
58+ private const string NGCLIENT_LOCATION = "ngclient/" ;
5559
5660private sealed record SpaConfig ( string Prefix , string FileContent , string BasePath ) ;
5761
5862private static readonly Regex _baseHrefRegex = new Regex (
59- @"(<base\b[^>]*?\bhref\s*=\s*)(['""])\s*/ \s*(?=\2)" ,
63+ @"(<base\b[^>]*?\bhref\s*=\s*)(['""])\s*[^'""]* \s*(?=\2)" ,
6064RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
6165
6266private static readonly Regex _headInjectRegex = new Regex (
6367@"(</head\s*>)" ,
6468RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ;
6569
66- private static string PatchIndexContent ( string fileContent , string prefix )
70+ private static string PatchIndexContent ( string fileContent , string prefix , string ngclientPrefix , bool enableIframeHosting )
6771{
6872if ( ! prefix . EndsWith ( "/" ) )
6973prefix += "/" ;
7074
75+ if ( string . IsNullOrWhiteSpace ( ngclientPrefix ) )
76+ ngclientPrefix = $ "{ prefix } { NGCLIENT_LOCATION } ";
77+
7178var headContent = string . Empty ;
7279if ( ! string . IsNullOrWhiteSpace ( AutoUpdateSettings . CustomCssFilePath ) )
7380headContent += $ "<link rel=\" stylesheet\" href=\" oem-custom.css\" />";
7481if ( ! string . IsNullOrWhiteSpace ( AutoUpdateSettings . CustomJsFilePath ) )
7582headContent += $ "<script src=\" oem-custom.js\" ></script>";
83+ if ( ! string . IsNullOrWhiteSpace ( prefix ) )
84+ headContent += $@ "<meta name=""duplicati-proxy-config"" content=""{ prefix } ""/> ";
85+ if ( enableIframeHosting )
86+ headContent += $@ "<meta name=""duplicati-enable-iframe-hosting"" content=""true""/> ";
87+
7688fileContent = _headInjectRegex . Replace ( fileContent , headContent + "</head>" ) ;
7789
7890return _baseHrefRegex . Replace ( fileContent , match=>
7991{
8092var quote = match . Groups [ 2 ] . Value ;
81- return $ "{ match . Groups [ 1 ] . Value } { quote } { prefix } ";
93+ return $ "{ match . Groups [ 1 ] . Value } { quote } { ngclientPrefix } ";
8294} ) ;
8395}
8496
@@ -120,7 +132,7 @@ public static IApplicationBuilder UseDefaultStaticFiles(this WebApplication app,
120132{
121133prefixHandlerMap . Add ( new SpaConfig ( prefix , File . ReadAllText ( fi . FullName ) , basepath ) ) ;
122134}
123- #ifDEBUG
135+ #ifDEBUG
124136else
125137{
126138// Install from NPM in debug mode for easier development
@@ -138,23 +150,10 @@ public static IApplicationBuilder UseDefaultStaticFiles(this WebApplication app,
138150prefixHandlerMap = prefixHandlerMap . OrderByDescending ( p=> p . Prefix . Length ) . ToList ( ) ;
139151app . Use ( async ( context , next ) =>
140152{
141- await next ( ) ;
142-
143- // Not found only
144- if ( context . Response . StatusCode != 404 || context . Response . HasStarted )
145- return ;
146-
147- // Check if we can use the path
148- var path = context . Request . Path ;
149- if ( ! path . HasValue )
150- return ;
151-
152153// Check if the path is a SPA path
153- var spaConfig = prefixHandlerMap . FirstOrDefault ( p=> path . Value . StartsWith ( p . Prefix ) ) ;
154- if ( spaConfig == null )
155- return ;
156-
157- if ( string . IsNullOrEmpty ( Path . GetExtension ( path ) ) || path . Value . EndsWith ( "/index.html" ) )
154+ var path = context . Request . Path ;
155+ var spaConfig = path . HasValue ? prefixHandlerMap . FirstOrDefault ( p=> path . Value . StartsWith ( p . Prefix ) ) : null ;
156+ if ( spaConfig != null && path . HasValue && ( string . IsNullOrEmpty ( Path . GetExtension ( path ) ) || path . Value . EndsWith ( "/index.html" ) ) )
158157{
159158// Serve the index file
160159context . Response . ContentType = "text/html" ;
@@ -165,13 +164,34 @@ public static IApplicationBuilder UseDefaultStaticFiles(this WebApplication app,
165164indexContent = File . ReadAllText ( Path . Combine ( spaConfig . BasePath , "index.html" ) ) ;
166165#endif
167166var forwardedPrefix = context . Request . Headers [ FORWARDED_PREFIX_HEADER ] . FirstOrDefault ( ) ;
168- if ( string . IsNullOrEmpty ( forwardedPrefix ) )
169- forwardedPrefix = DEFAULT_FORWARDED_PREFIX ;
167+ if ( string . IsNullOrWhiteSpace ( forwardedPrefix ) )
168+ forwardedPrefix = context . Request . Headers [ FORWARDED_PREFIX_HEADER_ALT ] . FirstOrDefault ( ) ;
169+ if ( string . IsNullOrWhiteSpace ( forwardedPrefix ) )
170+ forwardedPrefix = "" ;
171+
172+ var ngclientPrefix = context . Request . Headers [ FORWARDED_PREFIX_HEADER_NGCLIENT ] . FirstOrDefault ( ) ;
173+ if ( string . IsNullOrWhiteSpace ( ngclientPrefix ) )
174+ ngclientPrefix = "" ;
170175
171- await context . Response . WriteAsync ( PatchIndexContent ( indexContent , forwardedPrefix ) , context . RequestAborted ) ;
176+ var enableIframeHosting = Library . Utility . Utility . ParseBool ( context . Request . Headers [ ENABLE_IFRAME_HOSTING_HEADER ] . FirstOrDefault ( ) , false )
177+ || Library . Utility . Utility . ParseBool ( Environment . GetEnvironmentVariable ( ENABLE_IFRAME_HOSTING_ENVIRONMENT_VARIABLE ) , false ) ;
178+
179+ await context . Response . WriteAsync ( PatchIndexContent ( indexContent , forwardedPrefix , ngclientPrefix , enableIframeHosting ) , context . RequestAborted ) ;
172180await context . Response . CompleteAsync ( ) ;
181+ return ;
173182}
174- else if ( path . Value . EndsWith ( "/oem-custom.css" ) && ( customCssFile ? . Exists ?? false ) )
183+
184+ await next ( ) ;
185+
186+ // Handle not found only
187+ if ( context . Response . StatusCode != 404 || context . Response . HasStarted )
188+ return ;
189+
190+ // Check if we can use the path
191+ if ( ! path . HasValue )
192+ return ;
193+
194+ if ( path . Value . EndsWith ( "/oem-custom.css" ) && ( customCssFile ? . Exists ?? false ) )
175195{
176196// Serve the custom CSS file
177197context . Response . ContentType = "text/css" ;
@@ -187,7 +207,7 @@ public static IApplicationBuilder UseDefaultStaticFiles(this WebApplication app,
187207await context . Response . SendFileAsync ( new PhysicalFileInfo ( customJsFile ) ) ;
188208await context . Response . CompleteAsync ( ) ;
189209}
190- else
210+ else if ( spaConfig != null )
191211{
192212// Serve the static file
193213var file = new FileInfo ( Path . Combine ( spaConfig . BasePath , path . Value . Substring ( spaConfig . Prefix . Length ) . TrimStart ( '/' ) ) ) ;