@@ -175,10 +175,25 @@ public static void ValidateDocumentRoot(string documentRoot)
175175 throw new ArgumentException ( $ "DocumentRoot contains forbidden character: '{ c } '") ;
176176 }
177177
178+ public static void ValidateBindAddress ( string ? bindAddress )
179+ {
180+ if ( string . IsNullOrWhiteSpace ( bindAddress ) || bindAddress . Trim ( ) == "*" )
181+ return ;
182+ if ( bindAddress . Length > 64 )
183+ throw new ArgumentException ( "BindAddress too long (max 64 chars)" ) ;
184+ var value = bindAddress . Trim ( ) ;
185+ if ( value . Any ( c => char . IsWhiteSpace ( c ) || c is '"' or '\' ' or ';' or '|' or '&' or '`' or '<' or '>' or '\0 ' ) )
186+ throw new ArgumentException ( "BindAddress contains forbidden characters" ) ;
187+ var unwrapped = value . StartsWith ( '[' ) && value . EndsWith ( ']' ) ? value [ 1 ..^ 1 ] : value ;
188+ if ( ! System . Net . IPAddress . TryParse ( unwrapped , out _ ) )
189+ throw new ArgumentException ( "BindAddress must be an IP address or '*'" ) ;
190+ }
191+
178192 public async Task < SiteConfig > CreateAsync ( SiteConfig site )
179193 {
180194 ValidateDomain ( site . Domain ) ;
181195 ValidateDocumentRoot ( site . DocumentRoot ) ;
196+ ValidateBindAddress ( site . BindAddress ) ;
182197 if ( site . Aliases is { Length : > 0 } )
183198 foreach ( var alias in site . Aliases )
184199 ValidateAlias ( alias ) ;
@@ -211,6 +226,7 @@ public async Task<SiteConfig> UpdateAsync(SiteConfig site)
211226 // via crafted PUT body). Same treatment as Delete/Create.
212227 ValidateDomain ( site . Domain ) ;
213228 ValidateDocumentRoot ( site . DocumentRoot ) ;
229+ ValidateBindAddress ( site . BindAddress ) ;
214230 if ( site . Aliases is { Length : > 0 } )
215231 foreach ( var alias in site . Aliases )
216232 ValidateAlias ( alias ) ;
@@ -334,13 +350,15 @@ public async Task GenerateVhostAsync(SiteConfig site)
334350 catch { /* ignore */ }
335351
336352 var apacheSettings = site . ApacheSettings ;
353+ var aliases = NormalizeAliases ( site . Domain , site . Aliases ) ;
337354
338355 var model = new
339356 {
340357 site = new
341358 {
342359 domain = site . Domain ,
343- aliases = site . Aliases ,
360+ aliases = aliases ,
361+ bind_address = FormatApacheBindAddress ( site . BindAddress ) ,
344362 root = site . DocumentRoot ,
345363 root_parent = rootParent ,
346364 php_ini_path = sitePhpIni ,
@@ -399,6 +417,34 @@ public async Task GenerateVhostAsync(SiteConfig site)
399417 return candidates . FirstOrDefault ( File . Exists ) ;
400418 }
401419
420+ public static string [ ] NormalizeAliases ( string domain , IEnumerable < string > ? aliases )
421+ {
422+ var result = new List < string > ( ) ;
423+ var seen = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
424+ foreach ( var alias in aliases ?? Array . Empty < string > ( ) )
425+ {
426+ var value = alias . Trim ( ) ;
427+ if ( ! string . IsNullOrWhiteSpace ( value ) && seen . Add ( value ) )
428+ result . Add ( value ) ;
429+ }
430+ if ( string . Equals ( domain , "localhost" , StringComparison . OrdinalIgnoreCase )
431+ && seen . Add ( "127.0.0.1" ) )
432+ {
433+ result . Add ( "127.0.0.1" ) ;
434+ }
435+ return result . ToArray ( ) ;
436+ }
437+
438+ public static string FormatApacheBindAddress ( string ? bindAddress )
439+ {
440+ if ( string . IsNullOrWhiteSpace ( bindAddress ) || bindAddress . Trim ( ) == "*" )
441+ return "*" ;
442+ var value = bindAddress . Trim ( ) ;
443+ if ( value . StartsWith ( '[' ) && value . EndsWith ( ']' ) )
444+ return value ;
445+ return value . Contains ( ':' ) ? $ "[{ value } ]" : value ;
446+ }
447+
402448 /// <summary>
403449 /// Detects the PHP framework powering a site by inspecting its document root
404450 /// and parent directory (Laravel/Symfony/Nette typically have docroot=public/ or www/).
0 commit comments