diff options
| author | WizardOfYendor1 <WizardOfYendor1@users.noreply.github.com> | 2026-07-17 12:02:09 -0400 |
|---|---|---|
| committer | WizardOfYendor1 <WizardOfYendor1@users.noreply.github.com> | 2026-07-17 14:59:46 -0400 |
| commit | 0c7428f13675d1b63234cdc3ef5c748eb998e8e9 (patch) | |
| tree | 091ebbf37dd8057fce2ab87a661a8e3889f9be0e | |
| parent | 97e666c56639c5b1f846e684dee391c3b62c0ac9 (diff) | |
Added verbose, rambling, log warning to help users with config issues (hoping to reduce false issues reports).
Also added a test to exercise it, which is perhaps silly but convenient.
| -rw-r--r-- | src/Jellyfin.Networking/Manager/NetworkManager.cs | 44 | ||||
| -rw-r--r-- | tests/Jellyfin.Networking.Tests/NetworkParseTests.cs | 89 |
2 files changed, 133 insertions, 0 deletions
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 69f36e9bb1..496c108cfd 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -491,6 +491,7 @@ public class NetworkManager : INetworkManager, IDisposable startupOverrideKey, true, true)); + WarnIfPublishedUrlBasePathDiffers(publishedServerUrls, config.BaseUrl); _publishedServerUrls = publishedServerUrls; return; } @@ -580,10 +581,53 @@ public class NetworkManager : INetworkManager, IDisposable } } + WarnIfPublishedUrlBasePathDiffers(publishedServerUrls, config.BaseUrl); _publishedServerUrls = publishedServerUrls; } } + /// <summary> + /// Warns when a full-URL published server override uses a public path that differs from the configured base + /// URL. Jellyfin appends the base URL to generated Live TV client URLs in this case, which can conflict with + /// reverse proxies that translate public request paths. Bare host/IP overrides are exempt because the base URL + /// is appended when the API URL is built from them. + /// </summary> + /// <param name="publishedServerUrls">The parsed published server URL overrides.</param> + /// <param name="baseUrl">The configured base URL, if any.</param> + private void WarnIfPublishedUrlBasePathDiffers(List<PublishedServerUriOverride> publishedServerUrls, string baseUrl) + { + if (string.IsNullOrEmpty(baseUrl)) + { + return; + } + + foreach (var overrideUri in publishedServerUrls.Select(x => x.OverrideUri).Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (!overrideUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + && !overrideUri.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (!Uri.TryCreate(overrideUri, UriKind.Absolute, out var uri)) + { + continue; + } + + var path = Uri.UnescapeDataString(uri.AbsolutePath).TrimEnd('/'); + if (path.EndsWith(baseUrl, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var publishedServerHost = uri.GetComponents(UriComponents.HostAndPort, UriFormat.Unescaped); + _logger.LogWarning( + "The published server URL for host '{PublishedServerHost}' does not end with the configured base URL '{BaseUrl}'. Jellyfin will append this base URL when generating Live TV client URLs. If your reverse proxy translates public paths, this may cause Live TV playback to fail. Update the Published Server URIs setting on the Networking page of the admin dashboard, the JELLYFIN_PublishedServerUrl environment variable / --published-server-url option, or the reverse proxy path mapping accordingly.", + publishedServerHost, + baseUrl); + } + } + private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) { if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal)) diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 5f7a0efe8a..d8cb9e1ac6 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -562,6 +562,95 @@ namespace Jellyfin.Networking.Tests Assert.Null(port); } + [Theory] + // Full-URL override with a different public path: warn about the Live TV fallback. + [InlineData("all=https://media.example.com", "/jellyfin", true)] + // Full-URL override that ends with the base URL (with and without a trailing slash): no warning. + [InlineData("all=https://media.example.com/jellyfin", "/jellyfin", false)] + [InlineData("all=https://media.example.com/jellyfin/", "/jellyfin", false)] + [InlineData("all=https://media.example.com/media/jellyfin", "/jellyfin", false)] + [InlineData("all=https://media.example.com/cool%20server", "/cool server", false)] + // A similar segment or a path following the base URL is a different public API base. + [InlineData("all=https://media.example.com/jellyfinx", "/jellyfin", true)] + [InlineData("all=https://media.example.com/jellyfin/media", "/jellyfin", true)] + // No base URL configured: there is no path to compare. + [InlineData("all=https://media.example.com", "", false)] + // Bare host overrides get the base URL appended when the API URL is built: no warning. + [InlineData("all=media.example.com", "/jellyfin", false)] + [InlineData("internal=http-proxy.lan:8097", "/jellyfin", false)] + // Keyword overrides go through the same check as "all". + [InlineData("internal=http://10.0.0.5:8096", "/jellyfin", true)] + public void InitializeOverrides_FullUrlPublicPathDiffersFromBaseUrl_LogsWarning(string publishedServers, string baseUrl, bool expectWarning) + { + var conf = new NetworkConfiguration + { + LocalNetworkSubnets = new[] { "192.168.1.0/24" }, + LocalNetworkAddresses = new[] { "eth16" }, + EnableIPv4 = true, + PublishedServerUriBySubnet = new[] { publishedServers }, + BaseUrl = baseUrl + }; + + var logger = new Mock<ILogger<NetworkManager>>(); + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16"; + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, logger.Object); + NetworkManager.MockNetworkSettings = string.Empty; + + VerifyBaseUrlWarning(logger, expectWarning ? Times.AtLeastOnce() : Times.Never()); + } + + /// <summary> + /// The JELLYFIN_PublishedServerUrl environment variable / --published-server-url option takes the + /// startup-configuration branch of <c>InitializeOverrides</c> and must funnel through the same + /// base URL check as the dashboard overrides. + /// </summary> + [Fact] + public void InitializeOverrides_StartupPublishedServerUrlPathDiffersFromBaseUrl_LogsWarningWithoutCredentials() + { + var conf = new NetworkConfiguration + { + LocalNetworkSubnets = new[] { "192.168.1.0/24" }, + LocalNetworkAddresses = new[] { "eth16" }, + EnableIPv4 = true, + BaseUrl = "/jellyfin" + }; + + var logger = new Mock<ILogger<NetworkManager>>(); + var startupConf = new Mock<IConfiguration>(); + startupConf.Setup(x => x[MediaBrowser.Controller.Extensions.ConfigurationExtensions.AddressOverrideKey]).Returns("https://user:password@media.example.com?access_token=secret#fragment"); + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16"; + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, logger.Object); + NetworkManager.MockNetworkSettings = string.Empty; + + VerifyBaseUrlWarning(logger, Times.AtLeastOnce()); + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny<EventId>(), + It.Is<It.IsAnyType>((state, _) => state.ToString()!.Contains("user", StringComparison.Ordinal) + || state.ToString()!.Contains("password", StringComparison.Ordinal) + || state.ToString()!.Contains("access_token", StringComparison.Ordinal) + || state.ToString()!.Contains("secret", StringComparison.Ordinal) + || state.ToString()!.Contains("fragment", StringComparison.Ordinal)), + It.IsAny<Exception?>(), + It.IsAny<Func<It.IsAnyType, Exception?, string>>()), + Times.Never()); + } + + private static void VerifyBaseUrlWarning(Mock<ILogger<NetworkManager>> logger, Times times) + { + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny<EventId>(), + It.Is<It.IsAnyType>((state, _) => state.ToString()!.Contains("Jellyfin will append this base URL when generating Live TV client URLs", StringComparison.Ordinal)), + It.IsAny<Exception?>(), + It.IsAny<Func<It.IsAnyType, Exception?, string>>()), + times); + } + /// <summary> /// <see cref="NetworkManager.GetBindAddress(HttpRequest, out int?)"/> is the piece of request-host /// normalization that a request-host-aware smart API URL policy relies on: it resolves the bind address |
