From 7f8d1bb60f9bf0dc94cf7cfb41b5b01f532a2b3a Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Thu, 15 Jan 2026 15:25:27 +0800 Subject: [PATCH 01/31] Split application settings and host settings --- src/ServicePulse/ConstantsFile.cs | 33 +- src/ServicePulse/Program.cs | 15 +- src/ServicePulse/ReverseProxy.cs | 19 +- src/ServicePulse/ServicePulseHostSettings.cs | 381 ++++++++++++++++++ src/ServicePulse/Settings.cs | 349 +--------------- .../WebApplicationBuilderExtensions.cs | 4 +- src/ServicePulse/WebApplicationExtensions.cs | 4 +- 7 files changed, 409 insertions(+), 396 deletions(-) create mode 100644 src/ServicePulse/ServicePulseHostSettings.cs diff --git a/src/ServicePulse/ConstantsFile.cs b/src/ServicePulse/ConstantsFile.cs index fc76fb95da..52db262904 100644 --- a/src/ServicePulse/ConstantsFile.cs +++ b/src/ServicePulse/ConstantsFile.cs @@ -8,41 +8,12 @@ public static string GetContent(Settings settings) { var version = GetVersionInformation(); - string serviceControlUrl; - string monitoringUrl; - - if (settings.EnableReverseProxy) - { - serviceControlUrl = "/api/"; - monitoringUrl = settings.MonitoringUri == null ? "!" : "/monitoring-api/"; - } - else - { - // When HTTPS is enabled, upgrade backend URLs to HTTPS - var scUri = settings.HttpsEnabled - ? UpgradeToHttps(settings.ServiceControlUri) - : settings.ServiceControlUri; - serviceControlUrl = scUri.ToString(); - - if (settings.MonitoringUri != null) - { - var mUri = settings.HttpsEnabled - ? UpgradeToHttps(settings.MonitoringUri) - : settings.MonitoringUri; - monitoringUrl = mUri.ToString(); - } - else - { - monitoringUrl = "!"; - } - } - var constantsFile = $$""" window.defaultConfig = { default_route: '{{settings.DefaultRoute}}', version: '{{version}}', - service_control_url: '{{serviceControlUrl}}', - monitoring_urls: ['{{monitoringUrl}}'], + service_control_url: '{{settings.ServiceControlUrl}}', + monitoring_urls: ['{{settings.MonitoringUrl ?? "!"}}'], showPendingRetry: {{(settings.ShowPendingRetry ? "true" : "false")}}, } """; diff --git a/src/ServicePulse/Program.cs b/src/ServicePulse/Program.cs index 9b4ca85024..9be35cf22d 100644 --- a/src/ServicePulse/Program.cs +++ b/src/ServicePulse/Program.cs @@ -5,23 +5,26 @@ var builder = WebApplication.CreateBuilder(args); var settings = Settings.GetFromEnvironmentVariables(); +var hostSettings = ServicePulseHostSettings.GetFromEnvironmentVariables(); + +hostSettings.UpdateApplicationSettings(ref settings); // Configure Kestrel for HTTPS if enabled -builder.ConfigureHttps(settings); +builder.ConfigureHttps(hostSettings); -if (settings.EnableReverseProxy) +if (hostSettings.EnableReverseProxy) { - var (routes, clusters) = ReverseProxy.GetConfiguration(settings); + var (routes, clusters) = ReverseProxy.GetConfiguration(ref settings); builder.Services.AddReverseProxy().LoadFromMemory(routes, clusters); } var app = builder.Build(); // Forwarded headers must be first in the pipeline for correct scheme/host detection -app.UseForwardedHeaders(settings); +app.UseForwardedHeaders(hostSettings); // HTTPS middleware (HSTS and redirect) -app.UseHttpsConfiguration(settings); +app.UseHttpsConfiguration(hostSettings); var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly, "wwwroot"); var fileProvider = new CompositeFileProvider(builder.Environment.WebRootFileProvider, manifestEmbeddedFileProvider); @@ -32,7 +35,7 @@ var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider }; app.UseStaticFiles(staticFileOptions); -if (settings.EnableReverseProxy) +if (hostSettings.EnableReverseProxy) { app.MapReverseProxy(); } diff --git a/src/ServicePulse/ReverseProxy.cs b/src/ServicePulse/ReverseProxy.cs index 61e11c833f..96d3b5f59f 100644 --- a/src/ServicePulse/ReverseProxy.cs +++ b/src/ServicePulse/ReverseProxy.cs @@ -5,22 +5,18 @@ static class ReverseProxy { - public static (List routes, List clusters) GetConfiguration(Settings settings) + public static (List routes, List clusters) GetConfiguration(ref Settings settings) { var routes = new List(); var clusters = new List(); - // When HTTPS is enabled on ServicePulse, assume ServiceControl also uses HTTPS - var serviceControlUri = settings.HttpsEnabled - ? UpgradeToHttps(settings.ServiceControlUri) - : settings.ServiceControlUri; var serviceControlInstance = new ClusterConfig { ClusterId = "serviceControlInstance", Destinations = new Dictionary { - { "instance", new DestinationConfig { Address = serviceControlUri.ToString() } } + { "instance", new DestinationConfig { Address = settings.ServiceControlUrl } } } }; var serviceControlRoute = new RouteConfig @@ -35,20 +31,16 @@ public static (List routes, List clusters) GetConfig clusters.Add(serviceControlInstance); routes.Add(serviceControlRoute); + settings = settings with { ServiceControlUrl = "/api/" }; - if (settings.MonitoringUri != null) + if (settings.MonitoringUrl != null) { - // When HTTPS is enabled on ServicePulse, assume Monitoring also uses HTTPS - var monitoringUri = settings.HttpsEnabled - ? UpgradeToHttps(settings.MonitoringUri) - : settings.MonitoringUri; - var monitoringInstance = new ClusterConfig { ClusterId = "monitoringInstance", Destinations = new Dictionary { - { "instance", new DestinationConfig { Address = monitoringUri.ToString() } } + { "instance", new DestinationConfig { Address = settings.MonitoringUrl } } } }; @@ -61,6 +53,7 @@ public static (List routes, List clusters) GetConfig clusters.Add(monitoringInstance); routes.Add(monitoringRoute); + settings = settings with { MonitoringUrl = "/monitoring-api/" }; } return (routes, clusters); diff --git a/src/ServicePulse/ServicePulseHostSettings.cs b/src/ServicePulse/ServicePulseHostSettings.cs new file mode 100644 index 0000000000..79642aeed8 --- /dev/null +++ b/src/ServicePulse/ServicePulseHostSettings.cs @@ -0,0 +1,381 @@ +namespace ServicePulse; + +using System.Net; +using Microsoft.Extensions.Logging; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; + +class ServicePulseHostSettings +{ + static readonly ILogger logger = LoggerUtil.CreateStaticLogger(); + + public required bool EnableReverseProxy { get; init; } + + /// + /// Indicates whether forwarded headers processing for reverse proxy scenarios is enabled. + /// + public required bool ForwardedHeadersEnabled { get; init; } + + /// + /// Indicates whether all proxies are trusted for forwarded headers. + /// + public required bool ForwardedHeadersTrustAllProxies { get; init; } + + /// + /// List of known proxy IP addresses for forwarded headers. + /// + public required IReadOnlyList ForwardedHeadersKnownProxies { get; init; } + + /// + /// List of known networks for forwarded headers. + /// + public required IReadOnlyList ForwardedHeadersKnownNetworks { get; init; } + + /// + /// Indicates whether HTTPS is enabled. + /// + public required bool HttpsEnabled { get; init; } + + /// + /// Path to the HTTPS certificate file. + /// + public required string? HttpsCertificatePath { get; init; } + + /// + /// Password for the HTTPS certificate. + /// + public required string? HttpsCertificatePassword { get; init; } + + /// + /// Indicates whether HTTP requests should be redirected to HTTPS. + /// + public required bool HttpsRedirectHttpToHttps { get; init; } + + /// + /// The HTTPS port to use. + /// + public required int? HttpsPort { get; init; } + + /// + /// Indicates whether HSTS is enabled. + /// + public required bool HttpsEnableHsts { get; init; } + + /// + /// The max age for HSTS in seconds. + /// + public required int HttpsHstsMaxAgeSeconds { get; init; } + + /// + /// Indicates whether HSTS should include subdomains. + /// + public required bool HttpsHstsIncludeSubDomains { get; init; } + + public static ServicePulseHostSettings GetFromEnvironmentVariables() + { + var enableReverseProxyValue = Environment.GetEnvironmentVariable("ENABLE_REVERSE_PROXY"); + + if (!bool.TryParse(enableReverseProxyValue, out var enableReverseProxy)) + { + enableReverseProxy = true; + } + + var forwardedHeadersEnabled = ParseBool( + Environment.GetEnvironmentVariable("SERVICEPULSE_FORWARDEDHEADERS_ENABLED"), + defaultValue: true); + + var forwardedHeadersTrustAllProxies = ParseBool( + Environment.GetEnvironmentVariable("SERVICEPULSE_FORWARDEDHEADERS_TRUSTALLPROXIES"), + defaultValue: true); + + var forwardedHeadersKnownProxies = ParseIpAddresses( + Environment.GetEnvironmentVariable("SERVICEPULSE_FORWARDEDHEADERS_KNOWNPROXIES")); + var forwardedHeadersKnownNetworks = ParseNetworks( + Environment.GetEnvironmentVariable("SERVICEPULSE_FORWARDEDHEADERS_KNOWNNETWORKS")); + + // If specific proxies or networks are configured, disable trust all proxies + if (forwardedHeadersKnownProxies.Count > 0 || forwardedHeadersKnownNetworks.Count > 0) + { + forwardedHeadersTrustAllProxies = false; + } + + // HTTPS settings + var httpsEnabled = ParseBool( + Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_ENABLED"), + defaultValue: false); + + var httpsCertificatePath = Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_CERTIFICATEPATH"); + + var httpsCertificatePassword = Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_CERTIFICATEPASSWORD"); + + var httpsRedirectHttpToHttps = ParseBool( + Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_REDIRECTHTTPTOHTTPS"), + defaultValue: false); + + var httpsPort = ParseNullableInt( + Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_PORT")); + + var httpsEnableHsts = ParseBool( + Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_ENABLEHSTS"), + defaultValue: false); + + var httpsHstsMaxAgeSeconds = ParseInt( + Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_HSTSMAXAGESECONDS"), + defaultValue: 31536000); // 1 year + + var httpsHstsIncludeSubDomains = ParseBool( + Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_HSTSINCLUDESUBDOMAINS"), + defaultValue: false); + + var settings = new ServicePulseHostSettings + { + EnableReverseProxy = enableReverseProxy, + ForwardedHeadersEnabled = forwardedHeadersEnabled, + ForwardedHeadersTrustAllProxies = forwardedHeadersTrustAllProxies, + ForwardedHeadersKnownProxies = forwardedHeadersKnownProxies, + ForwardedHeadersKnownNetworks = forwardedHeadersKnownNetworks, + HttpsEnabled = httpsEnabled, + HttpsCertificatePath = httpsCertificatePath, + HttpsCertificatePassword = httpsCertificatePassword, + HttpsRedirectHttpToHttps = httpsRedirectHttpToHttps, + HttpsPort = httpsPort, + HttpsEnableHsts = httpsEnableHsts, + HttpsHstsMaxAgeSeconds = httpsHstsMaxAgeSeconds, + HttpsHstsIncludeSubDomains = httpsHstsIncludeSubDomains + }; + + settings.LogForwardedHeadersConfiguration(); + settings.ValidateHttpsCertificateConfiguration(); + settings.LogHttpsConfiguration(); + + + return settings; + } + + static bool ParseBool(string? value, bool defaultValue) + { + if (bool.TryParse(value, out var result)) + { + return result; + } + return defaultValue; + } + + static int ParseInt(string? value, int defaultValue) + { + if (int.TryParse(value, out var result)) + { + return result; + } + return defaultValue; + } + + static int? ParseNullableInt(string? value) + { + if (int.TryParse(value, out var result)) + { + return result; + } + return null; + } + + static IReadOnlyList ParseIpAddresses(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return []; + } + + var addresses = new List(); + var parts = value.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (var part in parts) + { + if (IPAddress.TryParse(part, out var address)) + { + addresses.Add(address); + } + } + + return addresses; + } + + static IReadOnlyList ParseNetworks(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return []; + } + + var networks = new List(); + var parts = value.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (var part in parts) + { + if (IPNetwork.TryParse(part, out var network)) + { + networks.Add(network); + } + } + + return networks; + } + + /// + /// Logs the forwarded headers configuration and warns about potential misconfigurations. + /// + void LogForwardedHeadersConfiguration() + { + var hasProxyConfig = ForwardedHeadersKnownProxies.Count > 0 || ForwardedHeadersKnownNetworks.Count > 0; + var knownProxiesDisplay = ForwardedHeadersKnownProxies.Count > 0 + ? string.Join(", ", ForwardedHeadersKnownProxies) + : "(none)"; + var knownNetworksDisplay = ForwardedHeadersKnownNetworks.Count > 0 + ? string.Join(", ", ForwardedHeadersKnownNetworks) + : "(none)"; + + logger.LogInformation("Forwarded headers settings: Enabled={Enabled}, TrustAllProxies={TrustAllProxies}, KnownProxies={KnownProxies}, KnownNetworks={KnownNetworks}", + ForwardedHeadersEnabled, ForwardedHeadersTrustAllProxies, knownProxiesDisplay, knownNetworksDisplay); + + // Warn about potential misconfigurations + if (!ForwardedHeadersEnabled && hasProxyConfig) + { + logger.LogWarning("Forwarded headers processing is disabled but proxy configuration is present. SERVICEPULSE_FORWARDEDHEADERS_KNOWNPROXIES and SERVICEPULSE_FORWARDEDHEADERS_KNOWNNETWORKS settings will be ignored"); + } + + if (ForwardedHeadersEnabled && ForwardedHeadersTrustAllProxies) + { + logger.LogWarning("Forwarded headers is configured to trust all proxies. Any client can spoof X-Forwarded-* headers. Consider configuring SERVICEPULSE_FORWARDEDHEADERS_KNOWNPROXIES or SERVICEPULSE_FORWARDEDHEADERS_KNOWNNETWORKS for production environments"); + } + + if (!ForwardedHeadersEnabled && ForwardedHeadersTrustAllProxies) + { + logger.LogWarning("Forwarded headers is disabled but TrustAllProxies is true. SERVICEPULSE_FORWARDEDHEADERS_TRUSTALLPROXIES setting will be ignored"); + } + + if (ForwardedHeadersEnabled && !ForwardedHeadersTrustAllProxies && !hasProxyConfig) + { + logger.LogWarning("Forwarded headers is enabled but no trusted proxies are configured. X-Forwarded-* headers will not be processed"); + } + + if (ForwardedHeadersEnabled && ForwardedHeadersKnownProxies.Count > 0 && ForwardedHeadersKnownNetworks.Count > 0) + { + logger.LogInformation("Forwarded headers has both KnownProxies and KnownNetworks configured. Both settings will be used to determine trusted proxies"); + } + } + + /// + /// Validates the HTTPS certificate configuration when HTTPS is enabled. + /// + /// Thrown when HTTPS is enabled but the certificate path is not set or the file does not exist. + void ValidateHttpsCertificateConfiguration() + { + if (!HttpsEnabled) + { + return; + } + + if (string.IsNullOrWhiteSpace(HttpsCertificatePath)) + { + var message = "SERVICEPULSE_HTTPS_CERTIFICATEPATH is required when HTTPS is enabled. Please specify the path to a valid HTTPS certificate file (.pfx or .pem)"; + logger.LogCritical(message); + throw new InvalidOperationException(message); + } + + if (!File.Exists(HttpsCertificatePath)) + { + var message = $"SERVICEPULSE_HTTPS_CERTIFICATEPATH does not exist. Current value: '{HttpsCertificatePath}'"; + logger.LogCritical(message); + throw new InvalidOperationException(message); + } + } + + /// + /// Logs the HTTPS configuration and warns about potential misconfigurations. + /// + void LogHttpsConfiguration() + { + var httpsPortDisplay = HttpsPort.HasValue ? HttpsPort.Value.ToString() : "(null)"; + + logger.LogInformation("HTTPS settings: Enabled={Enabled}, CertificatePath={CertificatePath}, HasCertificatePassword={HasCertificatePassword}, RedirectHttpToHttps={RedirectHttpToHttps}, HttpsPort={HttpsPort}, EnableHsts={EnableHsts}, HstsMaxAgeSeconds={HstsMaxAgeSeconds}, HstsIncludeSubDomains={HstsIncludeSubDomains}", + HttpsEnabled, HttpsCertificatePath, !string.IsNullOrEmpty(HttpsCertificatePassword), HttpsRedirectHttpToHttps, httpsPortDisplay, HttpsEnableHsts, HttpsHstsMaxAgeSeconds, HttpsHstsIncludeSubDomains); + + if (HttpsEnabled && EnableReverseProxy) + { + logger.LogInformation("HTTPS is enabled with reverse proxy. Backend connections to ServiceControl and Monitoring will be upgraded to HTTPS"); + } + + if (HttpsEnabled && !EnableReverseProxy) + { + logger.LogInformation("HTTPS is enabled without reverse proxy. ServiceControl and Monitoring URLs will be upgraded to HTTPS"); + } + + // Warn about potential misconfigurations + if (!HttpsEnabled) + { + logger.LogWarning("Kestrel HTTPS is disabled. Local communication will not be encrypted unless TLS is terminated by a reverse proxy"); + } + + if (!HttpsEnabled && (!string.IsNullOrEmpty(HttpsCertificatePath) || !string.IsNullOrEmpty(HttpsCertificatePassword))) + { + logger.LogWarning("HTTPS is disabled but certificate settings are provided. SERVICEPULSE_HTTPS_CERTIFICATEPATH and SERVICEPULSE_HTTPS_CERTIFICATEPASSWORD will be ignored"); + } + + if (HttpsRedirectHttpToHttps && !HttpsEnableHsts) + { + logger.LogWarning("HTTPS redirect is enabled but HSTS is disabled. Consider enabling SERVICEPULSE_HTTPS_ENABLEHSTS for better security"); + } + + if (!HttpsEnabled && HttpsEnableHsts) + { + logger.LogWarning("HSTS is enabled but Kestrel HTTPS is disabled. HSTS headers will only be effective if TLS is terminated by a reverse proxy"); + } + + if (!HttpsEnabled && HttpsRedirectHttpToHttps) + { + logger.LogWarning("HTTPS redirect is enabled but Kestrel HTTPS is disabled. Redirect will only work if TLS is terminated by a reverse proxy"); + } + + if (HttpsPort.HasValue && !HttpsRedirectHttpToHttps) + { + logger.LogWarning("SERVICEPULSE_HTTPS_PORT is configured but HTTPS redirect is disabled. The port setting will be ignored"); + } + + if (HttpsRedirectHttpToHttps && !HttpsPort.HasValue) + { + logger.LogInformation("SERVICEPULSE_HTTPS_PORT is not configured. HTTPS redirect will be ignored"); + } + } + + public void UpdateApplicationSettings(ref Settings settings) + { + // When HTTPS is enabled on ServicePulse, assume ServiceControl (and Monitoring, if configured) also uses HTTPS + if (HttpsEnabled) + { + settings = settings with + { + ServiceControlUrl = UpgradeToHttps(settings.ServiceControlUrl), + MonitoringUrl = settings.MonitoringUrl is not null + ? UpgradeToHttps(settings.MonitoringUrl) + : null + }; + } + } + + static string UpgradeToHttps(string url) + { + var uri = new Uri(url); + + if (uri.Scheme == Uri.UriSchemeHttps) + { + return url; + } + + var builder = new UriBuilder(uri) + { + Scheme = Uri.UriSchemeHttps, + Port = uri.IsDefaultPort ? -1 : uri.Port + }; + + return builder.Uri.ToString(); + } +} diff --git a/src/ServicePulse/Settings.cs b/src/ServicePulse/Settings.cs index 494136b38e..7cf884aaed 100644 --- a/src/ServicePulse/Settings.cs +++ b/src/ServicePulse/Settings.cs @@ -1,84 +1,17 @@ namespace ServicePulse; -using System.Net; using System.Text.Json; -using Microsoft.Extensions.Logging; -using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; -class Settings +record Settings { - static readonly ILogger logger = LoggerUtil.CreateStaticLogger(); + public required string ServiceControlUrl { get; init; } - public required Uri ServiceControlUri { get; init; } - - public required Uri? MonitoringUri { get; init; } + public required string? MonitoringUrl { get; init; } public required string DefaultRoute { get; init; } public required bool ShowPendingRetry { get; init; } - public required bool EnableReverseProxy { get; init; } - - /// - /// Indicates whether forwarded headers processing for reverse proxy scenarios is enabled. - /// - public required bool ForwardedHeadersEnabled { get; init; } - - /// - /// Indicates whether all proxies are trusted for forwarded headers. - /// - public required bool ForwardedHeadersTrustAllProxies { get; init; } - - /// - /// List of known proxy IP addresses for forwarded headers. - /// - public required IReadOnlyList ForwardedHeadersKnownProxies { get; init; } - - /// - /// List of known networks for forwarded headers. - /// - public required IReadOnlyList ForwardedHeadersKnownNetworks { get; init; } - - /// - /// Indicates whether HTTPS is enabled. - /// - public required bool HttpsEnabled { get; init; } - - /// - /// Path to the HTTPS certificate file. - /// - public required string? HttpsCertificatePath { get; init; } - - /// - /// Password for the HTTPS certificate. - /// - public required string? HttpsCertificatePassword { get; init; } - - /// - /// Indicates whether HTTP requests should be redirected to HTTPS. - /// - public required bool HttpsRedirectHttpToHttps { get; init; } - - /// - /// The HTTPS port to use. - /// - public required int? HttpsPort { get; init; } - - /// - /// Indicates whether HSTS is enabled. - /// - public required bool HttpsEnableHsts { get; init; } - - /// - /// The max age for HSTS in seconds. - /// - public required int HttpsHstsMaxAgeSeconds { get; init; } - - /// - /// Indicates whether HSTS should include subdomains. - /// - public required bool HttpsHstsIncludeSubDomains { get; init; } - public static Settings GetFromEnvironmentVariables() { var serviceControlUrl = Environment.GetEnvironmentVariable("SERVICECONTROL_URL") ?? "http://localhost:33333/api/"; @@ -108,155 +41,13 @@ public static Settings GetFromEnvironmentVariables() var showPendingRetryValue = Environment.GetEnvironmentVariable("SHOW_PENDING_RETRY"); bool.TryParse(showPendingRetryValue, out var showPendingRetry); - var enableReverseProxyValue = Environment.GetEnvironmentVariable("ENABLE_REVERSE_PROXY"); - - if (!bool.TryParse(enableReverseProxyValue, out var enableReverseProxy)) + return new Settings { - enableReverseProxy = true; - } - - var forwardedHeadersEnabled = ParseBool( - Environment.GetEnvironmentVariable("SERVICEPULSE_FORWARDEDHEADERS_ENABLED"), - defaultValue: true); - - var forwardedHeadersTrustAllProxies = ParseBool( - Environment.GetEnvironmentVariable("SERVICEPULSE_FORWARDEDHEADERS_TRUSTALLPROXIES"), - defaultValue: true); - - var forwardedHeadersKnownProxies = ParseIpAddresses( - Environment.GetEnvironmentVariable("SERVICEPULSE_FORWARDEDHEADERS_KNOWNPROXIES")); - var forwardedHeadersKnownNetworks = ParseNetworks( - Environment.GetEnvironmentVariable("SERVICEPULSE_FORWARDEDHEADERS_KNOWNNETWORKS")); - - // If specific proxies or networks are configured, disable trust all proxies - if (forwardedHeadersKnownProxies.Count > 0 || forwardedHeadersKnownNetworks.Count > 0) - { - forwardedHeadersTrustAllProxies = false; - } - - // HTTPS settings - var httpsEnabled = ParseBool( - Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_ENABLED"), - defaultValue: false); - - var httpsCertificatePath = Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_CERTIFICATEPATH"); - - var httpsCertificatePassword = Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_CERTIFICATEPASSWORD"); - - var httpsRedirectHttpToHttps = ParseBool( - Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_REDIRECTHTTPTOHTTPS"), - defaultValue: false); - - var httpsPort = ParseNullableInt( - Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_PORT")); - - var httpsEnableHsts = ParseBool( - Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_ENABLEHSTS"), - defaultValue: false); - - var httpsHstsMaxAgeSeconds = ParseInt( - Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_HSTSMAXAGESECONDS"), - defaultValue: 31536000); // 1 year - - var httpsHstsIncludeSubDomains = ParseBool( - Environment.GetEnvironmentVariable("SERVICEPULSE_HTTPS_HSTSINCLUDESUBDOMAINS"), - defaultValue: false); - - var settings = new Settings - { - ServiceControlUri = serviceControlUri, - MonitoringUri = monitoringUri, + ServiceControlUrl = serviceControlUri.ToString(), + MonitoringUrl = monitoringUri?.ToString(), DefaultRoute = defaultRoute, - ShowPendingRetry = showPendingRetry, - EnableReverseProxy = enableReverseProxy, - ForwardedHeadersEnabled = forwardedHeadersEnabled, - ForwardedHeadersTrustAllProxies = forwardedHeadersTrustAllProxies, - ForwardedHeadersKnownProxies = forwardedHeadersKnownProxies, - ForwardedHeadersKnownNetworks = forwardedHeadersKnownNetworks, - HttpsEnabled = httpsEnabled, - HttpsCertificatePath = httpsCertificatePath, - HttpsCertificatePassword = httpsCertificatePassword, - HttpsRedirectHttpToHttps = httpsRedirectHttpToHttps, - HttpsPort = httpsPort, - HttpsEnableHsts = httpsEnableHsts, - HttpsHstsMaxAgeSeconds = httpsHstsMaxAgeSeconds, - HttpsHstsIncludeSubDomains = httpsHstsIncludeSubDomains + ShowPendingRetry = showPendingRetry }; - - settings.LogForwardedHeadersConfiguration(); - settings.ValidateHttpsCertificateConfiguration(); - settings.LogHttpsConfiguration(); - - return settings; - } - - static bool ParseBool(string? value, bool defaultValue) - { - if (bool.TryParse(value, out var result)) - { - return result; - } - return defaultValue; - } - - static int ParseInt(string? value, int defaultValue) - { - if (int.TryParse(value, out var result)) - { - return result; - } - return defaultValue; - } - - static int? ParseNullableInt(string? value) - { - if (int.TryParse(value, out var result)) - { - return result; - } - return null; - } - - static IReadOnlyList ParseIpAddresses(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return []; - } - - var addresses = new List(); - var parts = value.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - foreach (var part in parts) - { - if (IPAddress.TryParse(part, out var address)) - { - addresses.Add(address); - } - } - - return addresses; - } - - static IReadOnlyList ParseNetworks(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return []; - } - - var networks = new List(); - var parts = value.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - foreach (var part in parts) - { - if (IPNetwork.TryParse(part, out var network)) - { - networks.Add(network); - } - } - - return networks; } static string? ParseLegacyMonitoringValue(string? value) @@ -294,130 +85,4 @@ class MonitoringUrls { public string[] Addresses { get; set; } = []; } - - /// - /// Logs the forwarded headers configuration and warns about potential misconfigurations. - /// - void LogForwardedHeadersConfiguration() - { - var hasProxyConfig = ForwardedHeadersKnownProxies.Count > 0 || ForwardedHeadersKnownNetworks.Count > 0; - var knownProxiesDisplay = ForwardedHeadersKnownProxies.Count > 0 - ? string.Join(", ", ForwardedHeadersKnownProxies) - : "(none)"; - var knownNetworksDisplay = ForwardedHeadersKnownNetworks.Count > 0 - ? string.Join(", ", ForwardedHeadersKnownNetworks) - : "(none)"; - - logger.LogInformation("Forwarded headers settings: Enabled={Enabled}, TrustAllProxies={TrustAllProxies}, KnownProxies={KnownProxies}, KnownNetworks={KnownNetworks}", - ForwardedHeadersEnabled, ForwardedHeadersTrustAllProxies, knownProxiesDisplay, knownNetworksDisplay); - - // Warn about potential misconfigurations - if (!ForwardedHeadersEnabled && hasProxyConfig) - { - logger.LogWarning("Forwarded headers processing is disabled but proxy configuration is present. SERVICEPULSE_FORWARDEDHEADERS_KNOWNPROXIES and SERVICEPULSE_FORWARDEDHEADERS_KNOWNNETWORKS settings will be ignored"); - } - - if (ForwardedHeadersEnabled && ForwardedHeadersTrustAllProxies) - { - logger.LogWarning("Forwarded headers is configured to trust all proxies. Any client can spoof X-Forwarded-* headers. Consider configuring SERVICEPULSE_FORWARDEDHEADERS_KNOWNPROXIES or SERVICEPULSE_FORWARDEDHEADERS_KNOWNNETWORKS for production environments"); - } - - if (!ForwardedHeadersEnabled && ForwardedHeadersTrustAllProxies) - { - logger.LogWarning("Forwarded headers is disabled but TrustAllProxies is true. SERVICEPULSE_FORWARDEDHEADERS_TRUSTALLPROXIES setting will be ignored"); - } - - if (ForwardedHeadersEnabled && !ForwardedHeadersTrustAllProxies && !hasProxyConfig) - { - logger.LogWarning("Forwarded headers is enabled but no trusted proxies are configured. X-Forwarded-* headers will not be processed"); - } - - if (ForwardedHeadersEnabled && ForwardedHeadersKnownProxies.Count > 0 && ForwardedHeadersKnownNetworks.Count > 0) - { - logger.LogInformation("Forwarded headers has both KnownProxies and KnownNetworks configured. Both settings will be used to determine trusted proxies"); - } - } - - /// - /// Validates the HTTPS certificate configuration when HTTPS is enabled. - /// - /// Thrown when HTTPS is enabled but the certificate path is not set or the file does not exist. - void ValidateHttpsCertificateConfiguration() - { - if (!HttpsEnabled) - { - return; - } - - if (string.IsNullOrWhiteSpace(HttpsCertificatePath)) - { - var message = "SERVICEPULSE_HTTPS_CERTIFICATEPATH is required when HTTPS is enabled. Please specify the path to a valid HTTPS certificate file (.pfx or .pem)"; - logger.LogCritical(message); - throw new InvalidOperationException(message); - } - - if (!File.Exists(HttpsCertificatePath)) - { - var message = $"SERVICEPULSE_HTTPS_CERTIFICATEPATH does not exist. Current value: '{HttpsCertificatePath}'"; - logger.LogCritical(message); - throw new InvalidOperationException(message); - } - } - - /// - /// Logs the HTTPS configuration and warns about potential misconfigurations. - /// - void LogHttpsConfiguration() - { - var httpsPortDisplay = HttpsPort.HasValue ? HttpsPort.Value.ToString() : "(null)"; - - logger.LogInformation("HTTPS settings: Enabled={Enabled}, CertificatePath={CertificatePath}, HasCertificatePassword={HasCertificatePassword}, RedirectHttpToHttps={RedirectHttpToHttps}, HttpsPort={HttpsPort}, EnableHsts={EnableHsts}, HstsMaxAgeSeconds={HstsMaxAgeSeconds}, HstsIncludeSubDomains={HstsIncludeSubDomains}", - HttpsEnabled, HttpsCertificatePath, !string.IsNullOrEmpty(HttpsCertificatePassword), HttpsRedirectHttpToHttps, httpsPortDisplay, HttpsEnableHsts, HttpsHstsMaxAgeSeconds, HttpsHstsIncludeSubDomains); - - if (HttpsEnabled && EnableReverseProxy) - { - logger.LogInformation("HTTPS is enabled with reverse proxy. Backend connections to ServiceControl and Monitoring will be upgraded to HTTPS"); - } - - if (HttpsEnabled && !EnableReverseProxy) - { - logger.LogInformation("HTTPS is enabled without reverse proxy. ServiceControl and Monitoring URLs will be upgraded to HTTPS"); - } - - // Warn about potential misconfigurations - if (!HttpsEnabled) - { - logger.LogWarning("Kestrel HTTPS is disabled. Local communication will not be encrypted unless TLS is terminated by a reverse proxy"); - } - - if (!HttpsEnabled && (!string.IsNullOrEmpty(HttpsCertificatePath) || !string.IsNullOrEmpty(HttpsCertificatePassword))) - { - logger.LogWarning("HTTPS is disabled but certificate settings are provided. SERVICEPULSE_HTTPS_CERTIFICATEPATH and SERVICEPULSE_HTTPS_CERTIFICATEPASSWORD will be ignored"); - } - - if (HttpsRedirectHttpToHttps && !HttpsEnableHsts) - { - logger.LogWarning("HTTPS redirect is enabled but HSTS is disabled. Consider enabling SERVICEPULSE_HTTPS_ENABLEHSTS for better security"); - } - - if (!HttpsEnabled && HttpsEnableHsts) - { - logger.LogWarning("HSTS is enabled but Kestrel HTTPS is disabled. HSTS headers will only be effective if TLS is terminated by a reverse proxy"); - } - - if (!HttpsEnabled && HttpsRedirectHttpToHttps) - { - logger.LogWarning("HTTPS redirect is enabled but Kestrel HTTPS is disabled. Redirect will only work if TLS is terminated by a reverse proxy"); - } - - if (HttpsPort.HasValue && !HttpsRedirectHttpToHttps) - { - logger.LogWarning("SERVICEPULSE_HTTPS_PORT is configured but HTTPS redirect is disabled. The port setting will be ignored"); - } - - if (HttpsRedirectHttpToHttps && !HttpsPort.HasValue) - { - logger.LogInformation("SERVICEPULSE_HTTPS_PORT is not configured. HTTPS redirect will be ignored"); - } - } } diff --git a/src/ServicePulse/WebApplicationBuilderExtensions.cs b/src/ServicePulse/WebApplicationBuilderExtensions.cs index 1645af9da8..527c06e824 100644 --- a/src/ServicePulse/WebApplicationBuilderExtensions.cs +++ b/src/ServicePulse/WebApplicationBuilderExtensions.cs @@ -4,7 +4,7 @@ namespace ServicePulse; static class WebApplicationBuilderExtensions { - public static void ConfigureHttps(this WebApplicationBuilder builder, Settings settings) + public static void ConfigureHttps(this WebApplicationBuilder builder, ServicePulseHostSettings settings) { // EnableHsts is disabled by default // Hsts is automatically disabled in Development environments @@ -51,7 +51,7 @@ public static void ConfigureHttps(this WebApplicationBuilder builder, Settings s } } - static X509Certificate2 LoadCertificate(Settings settings) + static X509Certificate2 LoadCertificate(ServicePulseHostSettings settings) { var certPath = settings.HttpsCertificatePath ?? throw new InvalidOperationException("HTTPS is enabled but HTTPS_CERTIFICATEPATH is not set."); diff --git a/src/ServicePulse/WebApplicationExtensions.cs b/src/ServicePulse/WebApplicationExtensions.cs index 3f2f76da01..14f6a8c619 100644 --- a/src/ServicePulse/WebApplicationExtensions.cs +++ b/src/ServicePulse/WebApplicationExtensions.cs @@ -4,7 +4,7 @@ namespace ServicePulse; static class WebApplicationExtensions { - public static void UseForwardedHeaders(this WebApplication app, Settings settings) + public static void UseForwardedHeaders(this WebApplication app, ServicePulseHostSettings settings) { // Register debug endpoint first (before early return) so it's always available in Development if (app.Environment.IsDevelopment()) @@ -83,7 +83,7 @@ public static void UseForwardedHeaders(this WebApplication app, Settings setting app.UseForwardedHeaders(options); } - public static void UseHttpsConfiguration(this WebApplication app, Settings settings) + public static void UseHttpsConfiguration(this WebApplication app, ServicePulseHostSettings settings) { // EnableHsts is disabled by default // Hsts is automatically disabled in Development environments From e55481dec4557c2dbcd2d54aeea03008581e7978 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Thu, 15 Jan 2026 15:28:32 +0800 Subject: [PATCH 02/31] Extract App from Host --- src/ServicePulse/Program.cs | 18 +------------ .../ServicePulseHostingExtensions.cs | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 src/ServicePulse/ServicePulseHostingExtensions.cs diff --git a/src/ServicePulse/Program.cs b/src/ServicePulse/Program.cs index 9be35cf22d..7bc88ef4ac 100644 --- a/src/ServicePulse/Program.cs +++ b/src/ServicePulse/Program.cs @@ -1,5 +1,3 @@ -using System.Net.Mime; -using Microsoft.Extensions.FileProviders; using ServicePulse; var builder = WebApplication.CreateBuilder(args); @@ -26,27 +24,13 @@ // HTTPS middleware (HSTS and redirect) app.UseHttpsConfiguration(hostSettings); -var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly, "wwwroot"); -var fileProvider = new CompositeFileProvider(builder.Environment.WebRootFileProvider, manifestEmbeddedFileProvider); - -var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider }; -app.UseDefaultFiles(defaultFilesOptions); - -var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider }; -app.UseStaticFiles(staticFileOptions); if (hostSettings.EnableReverseProxy) { app.MapReverseProxy(); } -var constantsFile = ConstantsFile.GetContent(settings); - -app.MapGet("/js/app.constants.js", (HttpContext context) => -{ - context.Response.ContentType = MediaTypeNames.Text.JavaScript; - return constantsFile; -}); +app.UseServicePulse(settings, builder.Environment.WebRootFileProvider); app.Run(); diff --git a/src/ServicePulse/ServicePulseHostingExtensions.cs b/src/ServicePulse/ServicePulseHostingExtensions.cs new file mode 100644 index 0000000000..ac274ec34c --- /dev/null +++ b/src/ServicePulse/ServicePulseHostingExtensions.cs @@ -0,0 +1,26 @@ +using System.Net.Mime; +using Microsoft.Extensions.FileProviders; +using ServicePulse; + +static class ServicePulseHostingExtensions +{ + public static void UseServicePulse(this WebApplication app, Settings settings, IFileProvider overrideFileProvider) + { + var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly, "wwwroot"); + var fileProvider = new CompositeFileProvider(overrideFileProvider, manifestEmbeddedFileProvider); + + var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider }; + app.UseDefaultFiles(defaultFilesOptions); + + var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider }; + app.UseStaticFiles(staticFileOptions); + + var constantsFile = ConstantsFile.GetContent(settings); + + app.MapGet("/js/app.constants.js", (HttpContext context) => + { + context.Response.ContentType = MediaTypeNames.Text.JavaScript; + return constantsFile; + }); + } +} From 12235ac5a922551dfbe81d430974d2492cdcffed Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Thu, 15 Jan 2026 15:35:12 +0800 Subject: [PATCH 03/31] Extract core from host --- .../ConstantsFile.cs | 2 +- .../ServicePulse.Core.csproj | 23 +++++++++++++++++++ .../ServicePulseHostingExtensions.cs | 6 +++-- .../Settings.cs | 2 +- src/ServicePulse.sln | 10 ++++++-- src/ServicePulse/ServicePulse.csproj | 8 +++---- 6 files changed, 40 insertions(+), 11 deletions(-) rename src/{ServicePulse => ServicePulse.Core}/ConstantsFile.cs (91%) create mode 100644 src/ServicePulse.Core/ServicePulse.Core.csproj rename src/{ServicePulse => ServicePulse.Core}/ServicePulseHostingExtensions.cs (83%) rename src/{ServicePulse => ServicePulse.Core}/Settings.cs (99%) diff --git a/src/ServicePulse/ConstantsFile.cs b/src/ServicePulse.Core/ConstantsFile.cs similarity index 91% rename from src/ServicePulse/ConstantsFile.cs rename to src/ServicePulse.Core/ConstantsFile.cs index 52db262904..2a5fe06770 100644 --- a/src/ServicePulse/ConstantsFile.cs +++ b/src/ServicePulse.Core/ConstantsFile.cs @@ -25,7 +25,7 @@ static string GetVersionInformation() { var majorMinorPatch = "0.0.0"; - var attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(); + var attributes = typeof(ConstantsFile).Assembly.GetCustomAttributes(); foreach (var attribute in attributes) { diff --git a/src/ServicePulse.Core/ServicePulse.Core.csproj b/src/ServicePulse.Core/ServicePulse.Core.csproj new file mode 100644 index 0000000000..2e93112650 --- /dev/null +++ b/src/ServicePulse.Core/ServicePulse.Core.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + diff --git a/src/ServicePulse/ServicePulseHostingExtensions.cs b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs similarity index 83% rename from src/ServicePulse/ServicePulseHostingExtensions.cs rename to src/ServicePulse.Core/ServicePulseHostingExtensions.cs index ac274ec34c..2775e6ebcb 100644 --- a/src/ServicePulse/ServicePulseHostingExtensions.cs +++ b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs @@ -1,12 +1,14 @@ using System.Net.Mime; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.FileProviders; using ServicePulse; -static class ServicePulseHostingExtensions +public static class ServicePulseHostingExtensions { public static void UseServicePulse(this WebApplication app, Settings settings, IFileProvider overrideFileProvider) { - var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly, "wwwroot"); + var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(ServicePulseHostingExtensions).Assembly, "wwwroot"); var fileProvider = new CompositeFileProvider(overrideFileProvider, manifestEmbeddedFileProvider); var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider }; diff --git a/src/ServicePulse/Settings.cs b/src/ServicePulse.Core/Settings.cs similarity index 99% rename from src/ServicePulse/Settings.cs rename to src/ServicePulse.Core/Settings.cs index 7cf884aaed..069005690d 100644 --- a/src/ServicePulse/Settings.cs +++ b/src/ServicePulse.Core/Settings.cs @@ -2,7 +2,7 @@ using System.Text.Json; -record Settings +public record Settings { public required string ServiceControlUrl { get; init; } diff --git a/src/ServicePulse.sln b/src/ServicePulse.sln index 1114063682..6a96732f0b 100644 --- a/src/ServicePulse.sln +++ b/src/ServicePulse.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32319.34 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11312.151 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServicePulse.Host", "ServicePulse.Host\ServicePulse.Host.csproj", "{D120B791-BD1B-4E06-B4E1-69801A73209B}" EndProject @@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePulse", "ServicePuls EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePulse.Tests", "ServicePulse.Tests\ServicePulse.Tests.csproj", "{9B75F526-937E-4B25-A9F6-2862129993EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePulse.Core", "ServicePulse.Core\ServicePulse.Core.csproj", "{8FCA3827-719C-49B7-A143-E48461D60F3C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -94,6 +96,10 @@ Global {9B75F526-937E-4B25-A9F6-2862129993EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B75F526-937E-4B25-A9F6-2862129993EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B75F526-937E-4B25-A9F6-2862129993EB}.Release|Any CPU.Build.0 = Release|Any CPU + {8FCA3827-719C-49B7-A143-E48461D60F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FCA3827-719C-49B7-A143-E48461D60F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FCA3827-719C-49B7-A143-E48461D60F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FCA3827-719C-49B7-A143-E48461D60F3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ServicePulse/ServicePulse.csproj b/src/ServicePulse/ServicePulse.csproj index 4230dd8456..42ab61e064 100644 --- a/src/ServicePulse/ServicePulse.csproj +++ b/src/ServicePulse/ServicePulse.csproj @@ -1,10 +1,9 @@ - + net8.0 enable enable - true false @@ -13,13 +12,12 @@ - - + - + \ No newline at end of file From ed453f25c88360741fe0b3c705b55bfbb21d2ce6 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Thu, 15 Jan 2026 15:37:51 +0800 Subject: [PATCH 04/31] Add public API documentation --- .../ServicePulseHostingExtensions.cs | 6 ++++++ src/ServicePulse.Core/Settings.cs | 18 ++++++++++++++++++ src/ServicePulse/Program.cs | 1 - 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs index 2775e6ebcb..5c31aa5159 100644 --- a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs +++ b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs @@ -4,8 +4,14 @@ using Microsoft.Extensions.FileProviders; using ServicePulse; +/// +/// Extensions for hosting ServicePulse within a WebApplication. +/// public static class ServicePulseHostingExtensions { + /// + /// Adds ServicePulse static file serving and configuration endpoint to the WebApplication. + /// public static void UseServicePulse(this WebApplication app, Settings settings, IFileProvider overrideFileProvider) { var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(ServicePulseHostingExtensions).Assembly, "wwwroot"); diff --git a/src/ServicePulse.Core/Settings.cs b/src/ServicePulse.Core/Settings.cs index 069005690d..78c2b2c82d 100644 --- a/src/ServicePulse.Core/Settings.cs +++ b/src/ServicePulse.Core/Settings.cs @@ -2,16 +2,34 @@ using System.Text.Json; +/// +/// Application Settings for ServicePulse. +/// public record Settings { + /// + /// The location of the ServiceControl API. + /// public required string ServiceControlUrl { get; init; } + /// + /// The location of the ServiceControl Monitoring API. + /// public required string? MonitoringUrl { get; init; } + /// + /// The default route to navigate to from the root. + /// public required string DefaultRoute { get; init; } + /// + /// Flag to enable the pending retry feature. + /// public required bool ShowPendingRetry { get; init; } + /// + /// Loads the settings from environment variables. + /// public static Settings GetFromEnvironmentVariables() { var serviceControlUrl = Environment.GetEnvironmentVariable("SERVICECONTROL_URL") ?? "http://localhost:33333/api/"; diff --git a/src/ServicePulse/Program.cs b/src/ServicePulse/Program.cs index 7bc88ef4ac..1f6734be78 100644 --- a/src/ServicePulse/Program.cs +++ b/src/ServicePulse/Program.cs @@ -24,7 +24,6 @@ // HTTPS middleware (HSTS and redirect) app.UseHttpsConfiguration(hostSettings); - if (hostSettings.EnableReverseProxy) { app.MapReverseProxy(); From dca04e22943db8a3dff88228cfb0441e538dab8b Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Thu, 15 Jan 2026 15:38:30 +0800 Subject: [PATCH 05/31] Rename Settings to make it clear in other contexts --- src/ServicePulse.Core/ConstantsFile.cs | 2 +- src/ServicePulse.Core/ServicePulseHostingExtensions.cs | 2 +- .../{Settings.cs => ServicePulseSettings.cs} | 6 +++--- src/ServicePulse/Program.cs | 2 +- src/ServicePulse/ReverseProxy.cs | 2 +- src/ServicePulse/ServicePulseHostSettings.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) rename src/ServicePulse.Core/{Settings.cs => ServicePulseSettings.cs} (95%) diff --git a/src/ServicePulse.Core/ConstantsFile.cs b/src/ServicePulse.Core/ConstantsFile.cs index 2a5fe06770..0b4da54777 100644 --- a/src/ServicePulse.Core/ConstantsFile.cs +++ b/src/ServicePulse.Core/ConstantsFile.cs @@ -4,7 +4,7 @@ class ConstantsFile { - public static string GetContent(Settings settings) + public static string GetContent(ServicePulseSettings settings) { var version = GetVersionInformation(); diff --git a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs index 5c31aa5159..a42a1bcf7a 100644 --- a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs +++ b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs @@ -12,7 +12,7 @@ public static class ServicePulseHostingExtensions /// /// Adds ServicePulse static file serving and configuration endpoint to the WebApplication. /// - public static void UseServicePulse(this WebApplication app, Settings settings, IFileProvider overrideFileProvider) + public static void UseServicePulse(this WebApplication app, ServicePulseSettings settings, IFileProvider overrideFileProvider) { var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(ServicePulseHostingExtensions).Assembly, "wwwroot"); var fileProvider = new CompositeFileProvider(overrideFileProvider, manifestEmbeddedFileProvider); diff --git a/src/ServicePulse.Core/Settings.cs b/src/ServicePulse.Core/ServicePulseSettings.cs similarity index 95% rename from src/ServicePulse.Core/Settings.cs rename to src/ServicePulse.Core/ServicePulseSettings.cs index 78c2b2c82d..003c559c69 100644 --- a/src/ServicePulse.Core/Settings.cs +++ b/src/ServicePulse.Core/ServicePulseSettings.cs @@ -5,7 +5,7 @@ /// /// Application Settings for ServicePulse. /// -public record Settings +public record ServicePulseSettings { /// /// The location of the ServiceControl API. @@ -30,7 +30,7 @@ public record Settings /// /// Loads the settings from environment variables. /// - public static Settings GetFromEnvironmentVariables() + public static ServicePulseSettings GetFromEnvironmentVariables() { var serviceControlUrl = Environment.GetEnvironmentVariable("SERVICECONTROL_URL") ?? "http://localhost:33333/api/"; @@ -59,7 +59,7 @@ public static Settings GetFromEnvironmentVariables() var showPendingRetryValue = Environment.GetEnvironmentVariable("SHOW_PENDING_RETRY"); bool.TryParse(showPendingRetryValue, out var showPendingRetry); - return new Settings + return new ServicePulseSettings { ServiceControlUrl = serviceControlUri.ToString(), MonitoringUrl = monitoringUri?.ToString(), diff --git a/src/ServicePulse/Program.cs b/src/ServicePulse/Program.cs index 1f6734be78..69aea49208 100644 --- a/src/ServicePulse/Program.cs +++ b/src/ServicePulse/Program.cs @@ -2,7 +2,7 @@ var builder = WebApplication.CreateBuilder(args); -var settings = Settings.GetFromEnvironmentVariables(); +var settings = ServicePulseSettings.GetFromEnvironmentVariables(); var hostSettings = ServicePulseHostSettings.GetFromEnvironmentVariables(); hostSettings.UpdateApplicationSettings(ref settings); diff --git a/src/ServicePulse/ReverseProxy.cs b/src/ServicePulse/ReverseProxy.cs index 96d3b5f59f..bf02dd4010 100644 --- a/src/ServicePulse/ReverseProxy.cs +++ b/src/ServicePulse/ReverseProxy.cs @@ -5,7 +5,7 @@ static class ReverseProxy { - public static (List routes, List clusters) GetConfiguration(ref Settings settings) + public static (List routes, List clusters) GetConfiguration(ref ServicePulseSettings settings) { var routes = new List(); var clusters = new List(); diff --git a/src/ServicePulse/ServicePulseHostSettings.cs b/src/ServicePulse/ServicePulseHostSettings.cs index 79642aeed8..c5a295c3d0 100644 --- a/src/ServicePulse/ServicePulseHostSettings.cs +++ b/src/ServicePulse/ServicePulseHostSettings.cs @@ -346,7 +346,7 @@ void LogHttpsConfiguration() } } - public void UpdateApplicationSettings(ref Settings settings) + public void UpdateApplicationSettings(ref ServicePulseSettings settings) { // When HTTPS is enabled on ServicePulse, assume ServiceControl (and Monitoring, if configured) also uses HTTPS if (HttpsEnabled) From a137919d96b591c1578a236ff6bd290c754dade1 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Thu, 15 Jan 2026 15:49:32 +0800 Subject: [PATCH 06/31] Skip AutomaticVersionRange on MS Dependency --- src/ServicePulse.Core/ServicePulse.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServicePulse.Core/ServicePulse.Core.csproj b/src/ServicePulse.Core/ServicePulse.Core.csproj index 2e93112650..5fa951ef0c 100644 --- a/src/ServicePulse.Core/ServicePulse.Core.csproj +++ b/src/ServicePulse.Core/ServicePulse.Core.csproj @@ -8,7 +8,7 @@ - + From adea759d6636910beee85f5cef58f40772aed761 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Thu, 15 Jan 2026 15:55:17 +0800 Subject: [PATCH 07/31] Expect an additional nuget --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5dc363ea1d..d30c1ffa53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,7 @@ jobs: $nugetsCount = (Get-ChildItem -Recurse -File nugets).Count $expectedAssetsCount = 1 - $expectedNugetsCount = 1 + $expectedNugetsCount = 2 if ($assetsCount -ne $expectedAssetsCount) { From 411bd9c49a02d4dd91ac2f1ff875fc34a092e709 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 16 Jan 2026 09:04:20 +0800 Subject: [PATCH 08/31] Serving files off disk is optional --- src/ServicePulse.Core/ServicePulseHostingExtensions.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs index a42a1bcf7a..03a4a92062 100644 --- a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs +++ b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs @@ -12,10 +12,12 @@ public static class ServicePulseHostingExtensions /// /// Adds ServicePulse static file serving and configuration endpoint to the WebApplication. /// - public static void UseServicePulse(this WebApplication app, ServicePulseSettings settings, IFileProvider overrideFileProvider) + public static void UseServicePulse(this WebApplication app, ServicePulseSettings settings, IFileProvider? overrideFileProvider = null) { var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(ServicePulseHostingExtensions).Assembly, "wwwroot"); - var fileProvider = new CompositeFileProvider(overrideFileProvider, manifestEmbeddedFileProvider); + IFileProvider fileProvider = overrideFileProvider is null + ? manifestEmbeddedFileProvider + : new CompositeFileProvider(overrideFileProvider, manifestEmbeddedFileProvider); var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider }; app.UseDefaultFiles(defaultFilesOptions); From 1c846d4e5d7e1b2464b8f3f31fd5fa01242335e3 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 16 Jan 2026 09:28:57 +0800 Subject: [PATCH 09/31] Indicate to UI if running in embedded mode --- src/ServicePulse.Core/ConstantsFile.cs | 1 + src/ServicePulse.Core/ServicePulseSettings.cs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ServicePulse.Core/ConstantsFile.cs b/src/ServicePulse.Core/ConstantsFile.cs index 0b4da54777..f56a570b3e 100644 --- a/src/ServicePulse.Core/ConstantsFile.cs +++ b/src/ServicePulse.Core/ConstantsFile.cs @@ -15,6 +15,7 @@ public static string GetContent(ServicePulseSettings settings) service_control_url: '{{settings.ServiceControlUrl}}', monitoring_urls: ['{{settings.MonitoringUrl ?? "!"}}'], showPendingRetry: {{(settings.ShowPendingRetry ? "true" : "false")}}, + isEmbedded: {{(settings.IsEmbedded ? "true" : "false")}} } """; diff --git a/src/ServicePulse.Core/ServicePulseSettings.cs b/src/ServicePulse.Core/ServicePulseSettings.cs index 003c559c69..ce8afddf60 100644 --- a/src/ServicePulse.Core/ServicePulseSettings.cs +++ b/src/ServicePulse.Core/ServicePulseSettings.cs @@ -27,6 +27,11 @@ public record ServicePulseSettings /// public required bool ShowPendingRetry { get; init; } + /// + /// Flag to indicate if ServicePulse is running in embedded mode. + /// + public required bool IsEmbedded { get; init; } + /// /// Loads the settings from environment variables. /// @@ -64,7 +69,8 @@ public static ServicePulseSettings GetFromEnvironmentVariables() ServiceControlUrl = serviceControlUri.ToString(), MonitoringUrl = monitoringUri?.ToString(), DefaultRoute = defaultRoute, - ShowPendingRetry = showPendingRetry + ShowPendingRetry = showPendingRetry, + IsEmbedded = false }; } From d9ae162ab8179d241b87dbfa37edfdeeed1332c6 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 16 Jan 2026 10:45:16 +0800 Subject: [PATCH 10/31] Adjust SP UI when embedded --- src/Frontend/env.d.ts | 1 + src/Frontend/public/js/app.constants.js | 1 + src/Frontend/src/components/PageFooter.vue | 6 ++++-- .../configuration/PlatformConnections.vue | 21 ++++++++++++------- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Frontend/env.d.ts b/src/Frontend/env.d.ts index 25bf2dc338..63511c0f87 100644 --- a/src/Frontend/env.d.ts +++ b/src/Frontend/env.d.ts @@ -10,6 +10,7 @@ declare global { service_control_url: string; monitoring_urls: string[]; showPendingRetry: boolean; + isEmbedded?: boolean; }; } } diff --git a/src/Frontend/public/js/app.constants.js b/src/Frontend/public/js/app.constants.js index d138f99109..4e49c18235 100644 --- a/src/Frontend/public/js/app.constants.js +++ b/src/Frontend/public/js/app.constants.js @@ -4,4 +4,5 @@ window.defaultConfig = { service_control_url: 'http://localhost:33333/api/', monitoring_urls: ['http://localhost:33633/'], showPendingRetry: false, + isEmbedded: true, }; diff --git a/src/Frontend/src/components/PageFooter.vue b/src/Frontend/src/components/PageFooter.vue index ecb826af4f..0d6f8ecd99 100644 --- a/src/Frontend/src/components/PageFooter.vue +++ b/src/Frontend/src/components/PageFooter.vue @@ -21,6 +21,7 @@ const environment = environmentAndVersionsStore.environment; const licenseStore = useLicenseStore(); const { licenseStatus, license } = licenseStore; const isMonitoringEnabled = monitoringClient.isMonitoringEnabled; +const isEmbedded = window.defaultConfig.isEmbedded; const scAddressTooltip = computed(() => { return `ServiceControl URL ${serviceControlClient.url}`; @@ -44,8 +45,9 @@ const { configuration } = storeToRefs(configurationStore); Connect new endpoint - ServicePulse v{{ environment.sp_version }} - + ServicePulse (EMBEDDED) + ServicePulse v{{ environment.sp_version }} + ServicePulse v{{ environment.sp_version }} ( v{{ newVersions.newSPVersion.newspversionnumber }} available) diff --git a/src/Frontend/src/components/configuration/PlatformConnections.vue b/src/Frontend/src/components/configuration/PlatformConnections.vue index 2f97ee54ea..02ccf41c68 100644 --- a/src/Frontend/src/components/configuration/PlatformConnections.vue +++ b/src/Frontend/src/components/configuration/PlatformConnections.vue @@ -20,6 +20,7 @@ const serviceControlValid = ref(null); const testingMonitoring = ref(false); const monitoringValid = ref(null); const connectionSaved = ref(null); +const isEmbedded = window.defaultConfig.isEmbedded; async function testServiceControlUrl() { if (localServiceControlUrl.value) { @@ -64,10 +65,15 @@ function saveConnections() { } function updateServiceControlUrls() { - if (!localServiceControlUrl.value) { - throw new Error("ServiceControl URL is mandatory"); - } else if (!localServiceControlUrl.value.endsWith("/")) { - localServiceControlUrl.value += "/"; + const params = new URLSearchParams(); + + if (!isEmbedded) { + if (!localServiceControlUrl.value) { + throw new Error("ServiceControl URL is mandatory"); + } else if (!localServiceControlUrl.value.endsWith("/")) { + localServiceControlUrl.value += "/"; + } + params.set("scu", localServiceControlUrl.value); } if (!localMonitoringUrl.value) { @@ -76,8 +82,6 @@ function updateServiceControlUrls() { localMonitoringUrl.value += "/"; } - const params = new URLSearchParams(); - params.set("scu", localServiceControlUrl.value); params.set("mu", localMonitoringUrl.value); window.location.search = `?${params.toString()}`; } @@ -94,11 +98,14 @@ function updateServiceControlUrls() {
- +
From 8e41237070de25d4b5c0b8174ffd8a6e19ba3e0f Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 16 Jan 2026 12:20:49 +0800 Subject: [PATCH 11/31] Cleanup --- src/Frontend/public/js/app.constants.js | 2 +- src/Frontend/src/components/PageFooter.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Frontend/public/js/app.constants.js b/src/Frontend/public/js/app.constants.js index 4e49c18235..8d65f8e78b 100644 --- a/src/Frontend/public/js/app.constants.js +++ b/src/Frontend/public/js/app.constants.js @@ -4,5 +4,5 @@ window.defaultConfig = { service_control_url: 'http://localhost:33333/api/', monitoring_urls: ['http://localhost:33633/'], showPendingRetry: false, - isEmbedded: true, + isEmbedded: false, }; diff --git a/src/Frontend/src/components/PageFooter.vue b/src/Frontend/src/components/PageFooter.vue index 0d6f8ecd99..3f3ece70f7 100644 --- a/src/Frontend/src/components/PageFooter.vue +++ b/src/Frontend/src/components/PageFooter.vue @@ -45,7 +45,7 @@ const { configuration } = storeToRefs(configurationStore); Connect new endpoint - ServicePulse (EMBEDDED) + ServicePulse: Embedded ServicePulse v{{ environment.sp_version }} ServicePulse v{{ environment.sp_version }} ( From 2337efa222a02ec7efe03a55a062da0910bfc60f Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 23 Jan 2026 12:17:53 +0800 Subject: [PATCH 12/31] Update StaticMiddlewareTests.cs --- src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs b/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs index f6036cf31e..97972d7e54 100644 --- a/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs +++ b/src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs @@ -129,7 +129,7 @@ public async Task Should_find_prefer_constants_file_on_disk_over_embedded_if_bot } }; await middleware.Invoke(context); - const long sizeOfFileOnDisk = 215; // this is the /app/js/app.constants.js file + const long sizeOfFileOnDisk = 237; // this is the /app/js/app.constants.js file Assert.That(context.Response.ContentLength, Is.EqualTo(sizeOfFileOnDisk)); Assert.That(context.Response.ContentType, Is.EqualTo("application/javascript")); } From 0564938e96a3496b89cd4cc25d4125448e90e0f8 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 23 Jan 2026 12:34:26 +0800 Subject: [PATCH 13/31] Use passed ServiceControl url if embedded --- src/Frontend/src/components/serviceControlClient.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Frontend/src/components/serviceControlClient.ts b/src/Frontend/src/components/serviceControlClient.ts index a5d025b9dd..d002c1a42f 100644 --- a/src/Frontend/src/components/serviceControlClient.ts +++ b/src/Frontend/src/components/serviceControlClient.ts @@ -96,6 +96,10 @@ class ServiceControlClient { } private getUrl() { + if (window.defaultConfig && window.defaultConfig.isEmbedded && window.defaultConfig.service_control_url && window.defaultConfig.service_control_url.length) { + return window.defaultConfig.service_control_url; + } + const searchParams = new URLSearchParams(window.location.search); const scu = searchParams.get("scu"); const existingScu = window.localStorage.getItem("scu"); From 924fccc25db5a1140a7eb662ec2dded4fc4c2e57 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Wed, 28 Jan 2026 13:58:18 +0800 Subject: [PATCH 14/31] Allow serving constants file anonymously --- src/ServicePulse.Core/ServicePulseHostingExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs index 03a4a92062..d3ad5356ab 100644 --- a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs +++ b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs @@ -31,6 +31,6 @@ public static void UseServicePulse(this WebApplication app, ServicePulseSettings { context.Response.ContentType = MediaTypeNames.Text.JavaScript; return constantsFile; - }); + }).AllowAnonymous(); } } From 689c3b12f26f3b02afe4d3080140446bb74133ab Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Wed, 28 Jan 2026 14:27:12 +0800 Subject: [PATCH 15/31] Remove unused method --- src/ServicePulse.Core/ConstantsFile.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/ServicePulse.Core/ConstantsFile.cs b/src/ServicePulse.Core/ConstantsFile.cs index f56a570b3e..6c7c209373 100644 --- a/src/ServicePulse.Core/ConstantsFile.cs +++ b/src/ServicePulse.Core/ConstantsFile.cs @@ -38,20 +38,4 @@ static string GetVersionInformation() return majorMinorPatch; } - - static Uri UpgradeToHttps(Uri uri) - { - if (uri.Scheme == Uri.UriSchemeHttps) - { - return uri; - } - - var builder = new UriBuilder(uri) - { - Scheme = Uri.UriSchemeHttps, - Port = uri.IsDefaultPort ? -1 : uri.Port - }; - - return builder.Uri; - } } From e40dbd9a2e48dace3482d822abe0447afc9398af Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Wed, 28 Jan 2026 14:30:08 +0800 Subject: [PATCH 16/31] Remove unused method 2 --- src/ServicePulse/ReverseProxy.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/ServicePulse/ReverseProxy.cs b/src/ServicePulse/ReverseProxy.cs index bf02dd4010..6714265951 100644 --- a/src/ServicePulse/ReverseProxy.cs +++ b/src/ServicePulse/ReverseProxy.cs @@ -58,20 +58,4 @@ public static (List routes, List clusters) GetConfig return (routes, clusters); } - - static Uri UpgradeToHttps(Uri uri) - { - if (uri.Scheme == Uri.UriSchemeHttps) - { - return uri; - } - - var builder = new UriBuilder(uri) - { - Scheme = Uri.UriSchemeHttps, - Port = uri.IsDefaultPort ? -1 : uri.Port - }; - - return builder.Uri; - } } From ec2027c05f459597ab46de296a7d0ad6329cbc9d Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Wed, 4 Feb 2026 12:37:50 +0800 Subject: [PATCH 17/31] Apply suggestions from code review Co-authored-by: Phil Bastian <155411597+PhilBastian@users.noreply.github.com> --- src/Frontend/src/components/PageFooter.vue | 6 ++++-- src/Frontend/src/components/serviceControlClient.ts | 2 +- src/ServicePulse.Core/ConstantsFile.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Frontend/src/components/PageFooter.vue b/src/Frontend/src/components/PageFooter.vue index 3f3ece70f7..37b0b62695 100644 --- a/src/Frontend/src/components/PageFooter.vue +++ b/src/Frontend/src/components/PageFooter.vue @@ -46,11 +46,13 @@ const { configuration } = storeToRefs(configurationStore); ServicePulse: Embedded - ServicePulse v{{ environment.sp_version }} - + Service Control: diff --git a/src/Frontend/src/components/serviceControlClient.ts b/src/Frontend/src/components/serviceControlClient.ts index d002c1a42f..bde561ea55 100644 --- a/src/Frontend/src/components/serviceControlClient.ts +++ b/src/Frontend/src/components/serviceControlClient.ts @@ -96,7 +96,7 @@ class ServiceControlClient { } private getUrl() { - if (window.defaultConfig && window.defaultConfig.isEmbedded && window.defaultConfig.service_control_url && window.defaultConfig.service_control_url.length) { + if (window.defaultConfig?.isEmbedded && window.defaultConfig.service_control_url?.length) { return window.defaultConfig.service_control_url; } diff --git a/src/ServicePulse.Core/ConstantsFile.cs b/src/ServicePulse.Core/ConstantsFile.cs index 6c7c209373..db4c830a93 100644 --- a/src/ServicePulse.Core/ConstantsFile.cs +++ b/src/ServicePulse.Core/ConstantsFile.cs @@ -15,7 +15,7 @@ public static string GetContent(ServicePulseSettings settings) service_control_url: '{{settings.ServiceControlUrl}}', monitoring_urls: ['{{settings.MonitoringUrl ?? "!"}}'], showPendingRetry: {{(settings.ShowPendingRetry ? "true" : "false")}}, - isEmbedded: {{(settings.IsEmbedded ? "true" : "false")}} + isEmbedded: {{(settings.IsEmbedded.ToString().ToLower())}} } """; From db6ba53902888727d5fe159f302da35794581a54 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Wed, 4 Feb 2026 12:40:28 +0800 Subject: [PATCH 18/31] Cleanup --- src/ServicePulse.Core/ConstantsFile.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ServicePulse.Core/ConstantsFile.cs b/src/ServicePulse.Core/ConstantsFile.cs index db4c830a93..b33ed027b9 100644 --- a/src/ServicePulse.Core/ConstantsFile.cs +++ b/src/ServicePulse.Core/ConstantsFile.cs @@ -14,8 +14,8 @@ public static string GetContent(ServicePulseSettings settings) version: '{{version}}', service_control_url: '{{settings.ServiceControlUrl}}', monitoring_urls: ['{{settings.MonitoringUrl ?? "!"}}'], - showPendingRetry: {{(settings.ShowPendingRetry ? "true" : "false")}}, - isEmbedded: {{(settings.IsEmbedded.ToString().ToLower())}} + showPendingRetry: {{settings.ShowPendingRetry.ToString().ToLower()}}, + isEmbedded: {{settings.IsEmbedded.ToString().ToLower()}} } """; From 381fb16648b90dcde10c0b8d6d8bb2e81c0991e4 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Wed, 4 Feb 2026 12:44:37 +0800 Subject: [PATCH 19/31] Fix whitespace --- src/Frontend/src/components/PageFooter.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Frontend/src/components/PageFooter.vue b/src/Frontend/src/components/PageFooter.vue index 37b0b62695..51409be3c2 100644 --- a/src/Frontend/src/components/PageFooter.vue +++ b/src/Frontend/src/components/PageFooter.vue @@ -46,13 +46,13 @@ const { configuration } = storeToRefs(configurationStore); ServicePulse: Embedded - + Service Control: From d6d360786e32a86a356b30c363b6f73ae14438ce Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 6 Feb 2026 12:22:05 +0800 Subject: [PATCH 20/31] Put hosting extension in namespace --- src/ServicePulse.Core/ServicePulseHostingExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs index d3ad5356ab..3e65383cdc 100644 --- a/src/ServicePulse.Core/ServicePulseHostingExtensions.cs +++ b/src/ServicePulse.Core/ServicePulseHostingExtensions.cs @@ -1,8 +1,9 @@ +namespace ServicePulse; + using System.Net.Mime; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.FileProviders; -using ServicePulse; /// /// Extensions for hosting ServicePulse within a WebApplication. From dea8d9ab34f9ab3624d32063493b90a024a6aa20 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Fri, 6 Feb 2026 12:28:23 +0800 Subject: [PATCH 21/31] Change embedded to integrated --- src/Frontend/env.d.ts | 2 +- src/Frontend/public/js/app.constants.js | 2 +- src/Frontend/src/components/PageFooter.vue | 4 ++-- .../components/configuration/PlatformConnections.vue | 10 +++++----- src/Frontend/src/components/serviceControlClient.ts | 2 +- src/ServicePulse.Core/ConstantsFile.cs | 2 +- src/ServicePulse.Core/ServicePulseSettings.cs | 6 +++--- .../Owin/StaticMiddlewareTests.cs | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Frontend/env.d.ts b/src/Frontend/env.d.ts index 63511c0f87..11ae739bab 100644 --- a/src/Frontend/env.d.ts +++ b/src/Frontend/env.d.ts @@ -10,7 +10,7 @@ declare global { service_control_url: string; monitoring_urls: string[]; showPendingRetry: boolean; - isEmbedded?: boolean; + isIntegrated?: boolean; }; } } diff --git a/src/Frontend/public/js/app.constants.js b/src/Frontend/public/js/app.constants.js index 8d65f8e78b..1c1a6c9e2f 100644 --- a/src/Frontend/public/js/app.constants.js +++ b/src/Frontend/public/js/app.constants.js @@ -4,5 +4,5 @@ window.defaultConfig = { service_control_url: 'http://localhost:33333/api/', monitoring_urls: ['http://localhost:33633/'], showPendingRetry: false, - isEmbedded: false, + isIntegrated: false, }; diff --git a/src/Frontend/src/components/PageFooter.vue b/src/Frontend/src/components/PageFooter.vue index 51409be3c2..f275a78c53 100644 --- a/src/Frontend/src/components/PageFooter.vue +++ b/src/Frontend/src/components/PageFooter.vue @@ -21,7 +21,7 @@ const environment = environmentAndVersionsStore.environment; const licenseStore = useLicenseStore(); const { licenseStatus, license } = licenseStore; const isMonitoringEnabled = monitoringClient.isMonitoringEnabled; -const isEmbedded = window.defaultConfig.isEmbedded; +const isIntegrated = window.defaultConfig.isIntegrated; const scAddressTooltip = computed(() => { return `ServiceControl URL ${serviceControlClient.url}`; @@ -45,7 +45,7 @@ const { configuration } = storeToRefs(configurationStore); Connect new endpoint - ServicePulse: Embedded + ServicePulse: Integrated