diff --git a/AppServiceProxy.Tests/ProxiesJsonReaderTests.cs b/AppServiceProxy.Tests/ProxiesJsonReaderTests.cs index 48715a2..40b2c72 100644 --- a/AppServiceProxy.Tests/ProxiesJsonReaderTests.cs +++ b/AppServiceProxy.Tests/ProxiesJsonReaderTests.cs @@ -1,4 +1,4 @@ -using AppServiceProxy.Configuration; +using AppServiceProxy.Configuration.Proxies; using Xunit; diff --git a/AppServiceProxy.Tests/ProxiesJsonTransformTests.cs b/AppServiceProxy.Tests/ProxiesJsonTransformTests.cs index d8e32aa..b4f9bad 100644 --- a/AppServiceProxy.Tests/ProxiesJsonTransformTests.cs +++ b/AppServiceProxy.Tests/ProxiesJsonTransformTests.cs @@ -1,6 +1,6 @@ using System; -using AppServiceProxy.Configuration; +using AppServiceProxy.Configuration.Proxies; using Xunit; diff --git a/AppServiceProxy.Tests/YarpJsonReaderTests.cs b/AppServiceProxy.Tests/YarpJsonReaderTests.cs new file mode 100644 index 0000000..9ee1fb6 --- /dev/null +++ b/AppServiceProxy.Tests/YarpJsonReaderTests.cs @@ -0,0 +1,39 @@ +using AppServiceProxy.Configuration.Yarp; + +using Xunit; + +namespace AppServiceProxy.Tests; + +public class YarpJsonReaderTests +{ + [Fact] + public void Basic() + { + var json = @" +{ + ""Routes"": { + ""route1"": { + ""ClusterId"": ""cluster1"", + ""Match"": { + ""Path"": ""{**catch-all}"" + } + } + }, + ""Clusters"": { + ""cluster1"": { + ""Destinations"": { + ""cluster1/destination1"": { + ""Address"": ""https://shibayan.jp/"" + } + } + } + } +} +"; + + var yarp = YarpJsonReader.ParseJson(json); + + Assert.Single(yarp.Routes); + Assert.Single(yarp.Clusters); + } +} diff --git a/AppServiceProxy/Configuration/FileBaseProxyConfigProvider.cs b/AppServiceProxy/Configuration/FileBaseProxyConfigProvider.cs new file mode 100644 index 0000000..2b34e5a --- /dev/null +++ b/AppServiceProxy/Configuration/FileBaseProxyConfigProvider.cs @@ -0,0 +1,77 @@ +using System.Diagnostics.CodeAnalysis; + +using AppServiceProxy.Configuration.Yarp; + +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +using Yarp.ReverseProxy.Configuration; + +namespace AppServiceProxy.Configuration; + +internal class FileBaseProxyConfigProvider : IProxyConfigProvider +{ + public FileBaseProxyConfigProvider(IEnumerable configFileLoaders) + { + _configFileLoaders = configFileLoaders; + + var json = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(@"C:\Users\shibayan\Documents\yarp.json")); + + _fileProvider = new PhysicalFileProvider(s_wwwroot) + { + UseActivePolling = true, + UsePollingFileWatcher = true + }; + } + + private readonly IEnumerable _configFileLoaders; + private readonly PhysicalFileProvider _fileProvider; + + private static readonly string s_wwwroot = Environment.ExpandEnvironmentVariables(@"%HOME%\site\wwwroot"); + + public IProxyConfig GetConfig() + { + if (TryGetConfigFileLoader(out var configFileLoader)) + { + var contents = File.ReadAllText(Path.Combine(s_wwwroot, configFileLoader.ConfigFileName)); + + var (routes, clusters) = configFileLoader.LoadConfig(contents); + + var changeToken = _fileProvider.Watch(configFileLoader.ConfigFileName); + + return new FileBaseProxyConfig + { + Routes = routes, + Clusters = clusters, + ChangeToken = changeToken + }; + } + else + { + var changeToken = _fileProvider.Watch("*.*"); + + return new FileBaseProxyConfig + { + Routes = Array.Empty(), + Clusters = Array.Empty(), + ChangeToken = changeToken + }; + } + } + + private bool TryGetConfigFileLoader([NotNullWhen(true)] out IConfigFileLoader? configFileLoader) + { + configFileLoader = _configFileLoaders.FirstOrDefault(x => File.Exists(Path.Combine(s_wwwroot, x.ConfigFileName))); + + return configFileLoader is not null; + } + + private class FileBaseProxyConfig : IProxyConfig + { + public IReadOnlyList Routes { get; init; } = null!; + + public IReadOnlyList Clusters { get; init; } = null!; + + public IChangeToken ChangeToken { get; init; } = null!; + } +} diff --git a/AppServiceProxy/Configuration/IConfigFileLoader.cs b/AppServiceProxy/Configuration/IConfigFileLoader.cs new file mode 100644 index 0000000..56d9e2e --- /dev/null +++ b/AppServiceProxy/Configuration/IConfigFileLoader.cs @@ -0,0 +1,10 @@ +using Yarp.ReverseProxy.Configuration; + +namespace AppServiceProxy.Configuration; + +internal interface IConfigFileLoader +{ + string ConfigFileName { get; } + + (IReadOnlyList, IReadOnlyList) LoadConfig(string contents); +} diff --git a/AppServiceProxy/Configuration/ProxiesJson.cs b/AppServiceProxy/Configuration/Proxies/ProxiesJson.cs similarity index 95% rename from AppServiceProxy/Configuration/ProxiesJson.cs rename to AppServiceProxy/Configuration/Proxies/ProxiesJson.cs index 7c94c29..3e89b66 100644 --- a/AppServiceProxy/Configuration/ProxiesJson.cs +++ b/AppServiceProxy/Configuration/Proxies/ProxiesJson.cs @@ -1,4 +1,4 @@ -namespace AppServiceProxy.Configuration; +namespace AppServiceProxy.Configuration.Proxies; internal class ProxiesJson { @@ -27,14 +27,18 @@ internal class MatchConditionConfig internal class RequestOverridesConfig { public string? Method { get; init; } + public IReadOnlyDictionary QueryString { get; init; } = null!; + public IReadOnlyDictionary Headers { get; init; } = null!; } internal class ResponseOverridesConfig { public int? StatusCode { get; init; } + public string? StatusReason { get; init; } + public IReadOnlyDictionary Headers { get; init; } = null!; } } diff --git a/AppServiceProxy/Configuration/Proxies/ProxiesJsonConfigFileLoader.cs b/AppServiceProxy/Configuration/Proxies/ProxiesJsonConfigFileLoader.cs new file mode 100644 index 0000000..62299dc --- /dev/null +++ b/AppServiceProxy/Configuration/Proxies/ProxiesJsonConfigFileLoader.cs @@ -0,0 +1,24 @@ + +using Yarp.ReverseProxy.Configuration; + +namespace AppServiceProxy.Configuration.Proxies; + +internal class ProxiesJsonConfigFileLoader : IConfigFileLoader +{ + public string ConfigFileName => "proxies.json"; + + public (IReadOnlyList, IReadOnlyList) LoadConfig(string contents) + { + try + { + var proxies = ProxiesJsonReader.ParseJson(contents); + var (routes, clusters) = ProxiesJsonTransform.Apply(proxies); + + return (routes, clusters); + } + catch + { + return (Array.Empty(), Array.Empty()); + } + } +} diff --git a/AppServiceProxy/Configuration/ProxiesJsonReader.cs b/AppServiceProxy/Configuration/Proxies/ProxiesJsonReader.cs similarity index 91% rename from AppServiceProxy/Configuration/ProxiesJsonReader.cs rename to AppServiceProxy/Configuration/Proxies/ProxiesJsonReader.cs index 919213a..06e7f82 100644 --- a/AppServiceProxy/Configuration/ProxiesJsonReader.cs +++ b/AppServiceProxy/Configuration/Proxies/ProxiesJsonReader.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using static AppServiceProxy.Configuration.ProxiesJson; +using static AppServiceProxy.Configuration.Proxies.ProxiesJson; -namespace AppServiceProxy.Configuration; +namespace AppServiceProxy.Configuration.Proxies; internal class ProxiesJsonReader { @@ -57,8 +57,8 @@ private static ProxyConfig ParseProxyConfig(JsonProperty proxy) var requestOverridesConfig = new RequestOverridesConfig { Method = requestOverrides.TryGetProperty("method", out var method) ? method.GetString() : null, - QueryString = requestOverrides.EnumerateObject().Where(x => x.Name.StartsWith("backend.request.querystring.")).ToDictionary(x => x.Name.Substring(28), x => x.Value.GetString()!), - Headers = requestOverrides.EnumerateObject().Where(x => x.Name.StartsWith("backend.request.headers.")).ToDictionary(x => x.Name.Substring(24), x => x.Value.GetString()!) + QueryString = requestOverrides.EnumerateObject().Where(x => x.Name.StartsWith("backend.request.querystring.")).ToDictionary(x => x.Name[28..], x => x.Value.GetString()!), + Headers = requestOverrides.EnumerateObject().Where(x => x.Name.StartsWith("backend.request.headers.")).ToDictionary(x => x.Name[24..], x => x.Value.GetString()!) }; return requestOverridesConfig; @@ -75,7 +75,7 @@ private static ProxyConfig ParseProxyConfig(JsonProperty proxy) { StatusCode = responseOverrides.TryGetProperty("response.statusCode", out var statusCode) ? statusCode.GetInt32() : null, StatusReason = responseOverrides.TryGetProperty("response.statusReason", out var statusReason) ? statusReason.GetString() : null, - Headers = responseOverrides.EnumerateObject().Where(x => x.Name.StartsWith("response.headers.")).ToDictionary(x => x.Name.Substring(17), x => x.Value.GetString()!) + Headers = responseOverrides.EnumerateObject().Where(x => x.Name.StartsWith("response.headers.")).ToDictionary(x => x.Name[17..], x => x.Value.GetString()!) }; return responseOverridesConfig; diff --git a/AppServiceProxy/Configuration/ProxiesJsonTransform.cs b/AppServiceProxy/Configuration/Proxies/ProxiesJsonTransform.cs similarity index 92% rename from AppServiceProxy/Configuration/ProxiesJsonTransform.cs rename to AppServiceProxy/Configuration/Proxies/ProxiesJsonTransform.cs index 725cbb6..767f3a3 100644 --- a/AppServiceProxy/Configuration/ProxiesJsonTransform.cs +++ b/AppServiceProxy/Configuration/Proxies/ProxiesJsonTransform.cs @@ -2,13 +2,13 @@ using Yarp.ReverseProxy.Configuration; -using static AppServiceProxy.Configuration.ProxiesJson; +using static AppServiceProxy.Configuration.Proxies.ProxiesJson; -namespace AppServiceProxy.Configuration; +namespace AppServiceProxy.Configuration.Proxies; -internal static class ProxiesJsonTransform +internal static partial class ProxiesJsonTransform { - private static readonly Regex s_templateRegex = new(@"\{([^\{\}]+)\}", RegexOptions.Compiled); + private static readonly Regex s_templateRegex = TemplateRegex(); public static (IReadOnlyList, IReadOnlyList) Apply(IReadOnlyList proxies) { @@ -126,4 +126,7 @@ private static (string, string?) SplitBackendUri(string backendUri) return (destinationAddress, absolutePath); } + + [GeneratedRegex(@"\{([^\{\}]+)\}", RegexOptions.Compiled)] + private static partial Regex TemplateRegex(); } diff --git a/AppServiceProxy/Configuration/ProxiesJsonFileConfigProvider.cs b/AppServiceProxy/Configuration/ProxiesJsonFileConfigProvider.cs deleted file mode 100644 index b8a3f1b..0000000 --- a/AppServiceProxy/Configuration/ProxiesJsonFileConfigProvider.cs +++ /dev/null @@ -1,59 +0,0 @@ - -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Primitives; - -using Yarp.ReverseProxy.Configuration; - -namespace AppServiceProxy.Configuration; - -internal class ProxiesJsonFileConfigProvider : IProxyConfigProvider -{ - private readonly PhysicalFileProvider _fileProvider = new(s_wwwroot) - { - UseActivePolling = true, - UsePollingFileWatcher = true - }; - - private const string ProxiesJsonFileName = "proxies.json"; - - private static readonly string s_wwwroot = Environment.ExpandEnvironmentVariables(@"%HOME%\site\wwwroot"); - - public IProxyConfig GetConfig() - { - var proxiesJsonFile = Path.Combine(s_wwwroot, ProxiesJsonFileName); - - var changeToken = _fileProvider.Watch(ProxiesJsonFileName); - - return new ProxiesJsonFileConfig(proxiesJsonFile, changeToken); - } - - private class ProxiesJsonFileConfig : IProxyConfig - { - public ProxiesJsonFileConfig(string proxiesJsonFile, IChangeToken changeToken) - { - try - { - var json = File.ReadAllText(proxiesJsonFile); - - var proxies = ProxiesJsonReader.ParseJson(json); - var (routes, clusters) = ProxiesJsonTransform.Apply(proxies); - - Routes = routes; - Clusters = clusters; - } - catch - { - Routes = Array.Empty(); - Clusters = Array.Empty(); - } - - ChangeToken = changeToken; - } - - public IReadOnlyList Routes { get; } - - public IReadOnlyList Clusters { get; } - - public IChangeToken ChangeToken { get; } - } -} diff --git a/AppServiceProxy/Configuration/Yarp/YarpJson.cs b/AppServiceProxy/Configuration/Yarp/YarpJson.cs new file mode 100644 index 0000000..6293b4a --- /dev/null +++ b/AppServiceProxy/Configuration/Yarp/YarpJson.cs @@ -0,0 +1,11 @@ + +using Yarp.ReverseProxy.Configuration; + +namespace AppServiceProxy.Configuration.Yarp; + +internal class YarpJson +{ + public IReadOnlyList Routes { get; set; } = null!; + + public IReadOnlyList Clusters { get; set; } = null!; +} diff --git a/AppServiceProxy/Configuration/Yarp/YarpJsonConfigFileLoader.cs b/AppServiceProxy/Configuration/Yarp/YarpJsonConfigFileLoader.cs new file mode 100644 index 0000000..4b62335 --- /dev/null +++ b/AppServiceProxy/Configuration/Yarp/YarpJsonConfigFileLoader.cs @@ -0,0 +1,23 @@ + +using Yarp.ReverseProxy.Configuration; + +namespace AppServiceProxy.Configuration.Yarp; + +internal class YarpJsonConfigFileLoader : IConfigFileLoader +{ + public string ConfigFileName => "yarp.json"; + + public (IReadOnlyList, IReadOnlyList) LoadConfig(string contents) + { + try + { + var yarp = YarpJsonReader.ParseJson(contents); + + return (yarp.Routes, yarp.Clusters); + } + catch + { + return (Array.Empty(), Array.Empty()); + } + } +} diff --git a/AppServiceProxy/Configuration/Yarp/YarpJsonReader.cs b/AppServiceProxy/Configuration/Yarp/YarpJsonReader.cs new file mode 100644 index 0000000..146d2f6 --- /dev/null +++ b/AppServiceProxy/Configuration/Yarp/YarpJsonReader.cs @@ -0,0 +1,53 @@ +using System.Text.Json; + +using Yarp.ReverseProxy.Configuration; + +namespace AppServiceProxy.Configuration.Yarp; + +internal class YarpJsonReader +{ + public static YarpJson ParseJson(string json) + { + var document = JsonDocument.Parse(json); + + var routes = document.RootElement.GetProperty("Routes"); + var clusters = document.RootElement.GetProperty("Clusters"); + + return new YarpJson { }; + } + + private static RouteConfig ParseRouteConfig(JsonProperty route) + { + return new RouteConfig + { + RouteId = route.Name, + Order = route.Value.TryGetProperty(nameof(RouteConfig.Order), out var order) ? order.GetInt32() : null, + ClusterId = route.Value.TryGetProperty(nameof(RouteConfig.ClusterId), out var clusterId) ? clusterId.GetString() : null, + AuthorizationPolicy = route.Value.TryGetProperty(nameof(RouteConfig.AuthorizationPolicy), out var authorizationPolicy) ? authorizationPolicy.GetString() : null, + CorsPolicy = route.Value.TryGetProperty(nameof(RouteConfig.CorsPolicy), out var corsPolicy) ? corsPolicy.GetString() : null, + Metadata = route.Value.TryGetProperty(nameof(RouteConfig.Metadata), out var metadata) ? metadata.Deserialize>() : null, + Transforms = route.Value.TryGetProperty(nameof(RouteConfig.Transforms), out var transforms) ? ParseTransforms(transforms) : null + }; + } + + private static ClusterConfig ParseClusterConfig(JsonProperty cluster) + { + return new ClusterConfig + { + ClusterId = cluster.Name, + LoadBalancingPolicy = cluster.Value.TryGetProperty(nameof(ClusterConfig.LoadBalancingPolicy), out var loadBalancingPolicy) ? loadBalancingPolicy.GetString() : null, + }; + } + + private static IReadOnlyList>? ParseTransforms(JsonElement transforms) + { + if (transforms.GetArrayLength() == 0) + { + return null; + } + + return transforms.EnumerateArray() + .Select(x => x.EnumerateObject().ToDictionary(xs => xs.Name, xs => xs.Value.GetString()!, StringComparer.OrdinalIgnoreCase)) + .ToList(); + } +} diff --git a/AppServiceProxy/Program.cs b/AppServiceProxy/Program.cs index d551939..c8c03f0 100644 --- a/AppServiceProxy/Program.cs +++ b/AppServiceProxy/Program.cs @@ -1,4 +1,8 @@ using AppServiceProxy.Configuration; +using AppServiceProxy.Configuration.Proxies; +using AppServiceProxy.Configuration.Yarp; + +using Microsoft.Extensions.DependencyInjection.Extensions; using Yarp.ReverseProxy.Configuration; @@ -6,7 +10,10 @@ builder.Services.AddReverseProxy(); -builder.Services.AddSingleton(); +builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); +builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/AppServiceProxy/web.config b/AppServiceProxy/web.config index b87f0a6..c62dfe0 100644 --- a/AppServiceProxy/web.config +++ b/AppServiceProxy/web.config @@ -14,4 +14,4 @@ - \ No newline at end of file + diff --git a/README.md b/README.md index e39587b..5514857 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,36 @@ See also [App Service Proxy Terraform module](https://github.com/shibayan/terraf ## Usage +### YARP + +Create `yarp.json` info `wwwroot` directory. + +```json +{ + "Routes": { + "route1" : { + "ClusterId": "cluster1", + "Match": { + "Path": "{**catch-all}" + } + } + }, + "Clusters": { + "cluster1": { + "Destinations": { + "cluster1/destination1": { + "Address": "https://shibayan.jp/" + } + } + } + } +} +``` + +- https://microsoft.github.io/reverse-proxy/articles/config-files.html + +### Azure Functions Proxies + Create `proxies.json` into `wwwroot` directory. ```json @@ -89,8 +119,6 @@ Create `proxies.json` into `wwwroot` directory. } ``` -## Appendix: `proxies.json` Reference - - [Advanced configuration - Work with proxies in Azure Functions | Microsoft Docs](https://docs.microsoft.com/en-us/azure/azure-functions/functions-proxies#advanced-configuration) ## License