Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match multiple ocelot routes for given downstream endpoint #284

Merged
merged 4 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private RouteDocs GetRouteDocs(RouteOptions route)
JToken paths = docs[OpenApiProperties.Paths];
string downstreamPath = GetDownstreamPath(route);
JProperty path = paths.OfType<JProperty>().FirstOrDefault(p =>
downstreamPath.Equals(p.Name.WithShashEnding(), StringComparison.CurrentCultureIgnoreCase));
downstreamPath.Equals(p.Name.WithSlashEnding(), StringComparison.OrdinalIgnoreCase));

return new RouteDocs()
{
Expand Down
4 changes: 2 additions & 2 deletions src/MMLib.SwaggerForOcelot/Configuration/RouteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
string upstreamPathTemplate,
string downstreamPathTemplate,
string virtualDirectory,
bool dangerousAcceptAnyServerCertificateValidator,

Check warning on line 43 in src/MMLib.SwaggerForOcelot/Configuration/RouteOptions.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Parameter 'dangerousAcceptAnyServerCertificateValidator' has no matching param tag in the XML comment for 'RouteOptions.RouteOptions(string, string, string, string, bool, IEnumerable<string>)' (but other parameters do)
IEnumerable<string> upstreamMethods) : this()
{
SwaggerKey = swaggerKey;
Expand Down Expand Up @@ -134,7 +134,7 @@
/// </summary>
public string DownstreamPath => DownstreamPathWithVirtualDirectory.RemoveSlashFromEnd();

internal string DownstreamPathWithSlash => DownstreamPathWithVirtualDirectory.WithShashEnding();
internal string DownstreamPathWithSlash => DownstreamPathWithVirtualDirectory.WithSlashEnding();

private readonly string _downstreamPathWithVirtualDirectory = null;

Expand Down Expand Up @@ -162,7 +162,7 @@
/// Gets a value indicating whether this instance can catch all.
/// </summary>
public bool CanCatchAll
=> DownstreamPathTemplate.EndsWith(CatchAllPlaceHolder, StringComparison.CurrentCultureIgnoreCase);
=> DownstreamPathTemplate.EndsWith(CatchAllPlaceHolder, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Gets the upstream path.
Expand Down
2 changes: 1 addition & 1 deletion src/MMLib.SwaggerForOcelot/MMLib.SwaggerForOcelot.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<Version>8.0.0</Version>
<Version>8.1.0</Version>
<Authors>Milan Martiniak</Authors>
<Company>MMLib</Company>
<Description>Swagger generator for Ocelot downstream services.</Description>
Expand Down
17 changes: 10 additions & 7 deletions src/MMLib.SwaggerForOcelot/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ internal static class StringExtensions
/// </summary>
/// <param name="value">The value.</param>
public static string RemoveSlashFromEnd(this string value)
=> value.TrimEnd().EndsWith("/")
? value.TrimEnd().TrimEnd('/')
: value;
=> value.TrimEnd().TrimEnd('/');

/// <summary>
/// Add slash to end.
/// </summary>
public static string WithShashEnding(this string value)
=> !value.TrimEnd().EndsWith("/")
? value + "/"
: value;
public static string WithSlashEnding(this string value)
{
value = value.TrimEnd();
if (!value.EndsWith('/'))
{
value += "/";
}
return value;
}
}
}
148 changes: 74 additions & 74 deletions src/MMLib.SwaggerForOcelot/Transformation/SwaggerJsonTransformer.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using Kros.IO;
using Kros.IO;
using Microsoft.Extensions.Caching.Memory;
using MMLib.SwaggerForOcelot.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
Expand All @@ -21,7 +20,7 @@
private readonly OcelotSwaggerGenOptions _ocelotSwaggerGenOptions;
private readonly IMemoryCache _memoryCache;

public SwaggerJsonTransformer(OcelotSwaggerGenOptions ocelotSwaggerGenOptions, IMemoryCache memoryCache)

Check warning on line 23 in src/MMLib.SwaggerForOcelot/Transformation/SwaggerJsonTransformer.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'SwaggerJsonTransformer.SwaggerJsonTransformer(OcelotSwaggerGenOptions, IMemoryCache)'
{
_ocelotSwaggerGenOptions = ocelotSwaggerGenOptions;
_memoryCache = memoryCache;
Expand Down Expand Up @@ -171,81 +170,54 @@

private void RenameAndRemovePaths(IEnumerable<RouteOptions> routes, JToken paths, string basePath)
{
var forRemove = new List<JProperty>();

var oldPaths = new List<JProperty>();
var newPaths = new Dictionary<string, JProperty>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < paths.Count(); i++)
{
var path = paths.ElementAt(i) as JProperty;
string downstreamPath = path.Name.RemoveSlashFromEnd();
RouteOptions route = FindRoute(routes, path.Name.WithShashEnding(), basePath);

if (route != null && RemoveMethods(path, route))
{
AddSecurityDefinitions(path, route);

RenameToken(path, ConvertDownstreamPathToUpstreamPath(downstreamPath, route.DownstreamPath, route.UpstreamPath, basePath));
}
else
var oldPath = paths.ElementAt(i) as JProperty;
oldPaths.Add(oldPath);
string downstreamPath = oldPath.Name.RemoveSlashFromEnd();
foreach (var tmpMethod in oldPath.First)
{
forRemove.Add(path);
var method = tmpMethod as JProperty;
List<RouteOptions> matchedRoutes = FindRoutes(routes, oldPath.Name.WithSlashEnding(), method.Name, basePath);
foreach (var route in matchedRoutes)
{
string newPath = ConvertDownstreamPathToUpstreamPath(
downstreamPath, route.DownstreamPath, route.UpstreamPath, basePath);
if (!newPaths.TryGetValue(newPath, out JProperty newPathJson))
{
newPathJson = new JProperty(newPath, new JObject());
newPaths.Add(newPath, newPathJson);
}
var newMethod = (method.DeepClone() as JProperty);
AddSecurityDefinition(newMethod, route);
((JObject)newPathJson.Value).Add(newMethod);
}
}
}

foreach (JProperty p in forRemove)
{
p.Remove();
}
oldPaths.ForEach(oldPath => oldPath.Remove());
newPaths.Select(item => item.Value)
.OrderBy(newPath => newPath.Name)
.ForEach(newPath => ((JObject)paths).Add(newPath));
}

private bool RemoveMethods(JProperty path, RouteOptions route)
{
var forRemove = new List<JProperty>();
var method = path.First.First as JProperty;

while (method != null)
{
if (!route.ContainsHttpMethod(method.Name))
{
forRemove.Add(method);
}
method = method.Next as JProperty;
}

foreach (JProperty m in forRemove)
{
m.Remove();
}

return path.First.Any();
}

private void AddSecurityDefinitions(JProperty path, RouteOptions route)
private void AddSecurityDefinition(JProperty method, RouteOptions route)
{
var authProviderKey = route.AuthenticationOptions?.AuthenticationProviderKey;

if (string.IsNullOrEmpty(authProviderKey))
{
return;
}

if (_ocelotSwaggerGenOptions.AuthenticationProviderKeyMap.TryGetValue(
authProviderKey,
out var securityScheme))
if (_ocelotSwaggerGenOptions.AuthenticationProviderKeyMap.TryGetValue(authProviderKey, out var securityScheme))
{
var method = path.First.First as JProperty;

while (method != null)
{
var securityProperty = new JProperty(OpenApiProperties.Security,
new JArray(
new JObject(
new JProperty(securityScheme,
new JArray(route.AuthenticationOptions?.AllowedScopes?.ToArray() ?? Array.Empty<string>())))));

((JObject)method.Value).Add(securityProperty);

method = method.Next as JProperty;
}
var securityProperty = new JProperty(OpenApiProperties.Security,
new JArray(
new JObject(
new JProperty(securityScheme,
new JArray(route.AuthenticationOptions?.AllowedScopes?.ToArray() ?? [])))));
((JObject)method.Value).Add(securityProperty);
}
}

Expand Down Expand Up @@ -297,13 +269,47 @@
}
}

private static RouteOptions FindRoute(IEnumerable<RouteOptions> routes, string downstreamPath, string basePath)
private static List<RouteOptions> FindRoutes(
IEnumerable<RouteOptions> routes,
string downstreamPath,
string method,
string basePath)
{
static bool MatchPaths(RouteOptions route, string downstreamPath)
=> route.CanCatchAll
? downstreamPath.StartsWith(route.DownstreamPathWithSlash, StringComparison.OrdinalIgnoreCase)
: route.DownstreamPathWithSlash.Equals(downstreamPath, StringComparison.OrdinalIgnoreCase);

string downstreamPathWithBasePath = PathHelper.BuildPath(basePath, downstreamPath);
return routes.FirstOrDefault(p
=> p.CanCatchAll
? downstreamPathWithBasePath.StartsWith(p.DownstreamPathWithSlash, StringComparison.CurrentCultureIgnoreCase)
: p.DownstreamPathWithSlash.Equals(downstreamPathWithBasePath, StringComparison.CurrentCultureIgnoreCase));
var matchedRoutes = routes
.Where(route => route.ContainsHttpMethod(method) && MatchPaths(route, downstreamPathWithBasePath))
.ToList();

RemoveRedundantRoutes(matchedRoutes);
return matchedRoutes;
}

// Redundant routes are routes with the ALMOST same upstream path templates. For example these path templates
// are redundant:
// - /api/projects/Projects
// - /api/projects/Projects/
// - /api/projects/Projects/{everything}
//
// `route.UpstreamPath` contains route without trailing slash and without catch-all placeholder, so all previous
// routes have the same upstream path `/api/projects/Projects`. The logic is to keep just the shortestof the path
// templates. If we would keep all routes, it will throw an exception during the generation of the swagger document
// later because of the same paths.
private static void RemoveRedundantRoutes(List<RouteOptions> routes)
{
IEnumerable<IGrouping<string, RouteOptions>> groups = routes
.GroupBy(route => route.UpstreamPath, StringComparer.OrdinalIgnoreCase)
.Where(group => group.Count() > 1);
foreach (var group in groups)
{
group.OrderBy(r => r.DownstreamPathTemplate.Length)
.Skip(1)
.ForEach(r => routes.Remove(r));
}
}

private static void AddHost(JObject swagger, string swaggerHost)
Expand All @@ -325,20 +331,14 @@
downstreamPath = PathHelper.BuildPath(downstreamBasePath, downstreamPath);
}

int pos = downstreamPath.IndexOf(downstreamPattern, StringComparison.CurrentCultureIgnoreCase);
int pos = downstreamPath.IndexOf(downstreamPattern, StringComparison.OrdinalIgnoreCase);
if (pos < 0)
{
return downstreamPath;
}
return $"{downstreamPath.Substring(0, pos)}{upstreamPattern}{downstreamPath.Substring(pos + downstreamPattern.Length)}";
}

private static void RenameToken(JProperty property, string newName)
{
var newProperty = new JProperty(newName, property.Value);
property.Replace(newProperty);
}

private static void TransformServerPaths(JObject openApi, string serverOverride, bool takeServersFromDownstreamService)
{
if (!openApi.ContainsKey(OpenApiProperties.Servers) || takeServersFromDownstreamService)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
</ItemGroup>
<ItemGroup>
<None Remove="Resources\AggregatesOpenApiResource.json" />
<None Remove="Resources\DifferentOcelotRoutesForOneDownstream.json" />
<None Remove="Resources\DifferentOcelotRoutesForOneDownstreamTransformed.json" />
<None Remove="Tests\BasicConfigurationWithSchemaInHostOverride.json" />
<None Remove="Tests\Issue_128.json" />
<None Remove="Tests\Issue_135.json" />
Expand All @@ -20,6 +22,8 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\AggregatesOpenApiResource.json" />
<EmbeddedResource Include="Resources\DifferentOcelotRoutesForOneDownstream.json" />
<EmbeddedResource Include="Resources\DifferentOcelotRoutesForOneDownstreamTransformed.json" />
<EmbeddedResource Include="Resources\OpenApiWithVersionPlaceholderBase.json" />
<EmbeddedResource Include="Resources\OpenApiWithVersionPlaceholderBaseTransformed.json" />
<EmbeddedResource Include="Resources\OpenApiBase.json" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"openapi": "3.0.1",
"info": {
"title": "WebApplication1",
"version": "1.0"
},
"paths": {
"/api/test": {
"get": {
"tags": [
"WebApplication1"
],
"operationId": "testGet",
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
},
"post": {
"tags": [
"WebApplication1"
],
"operationId": "testPost",
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": {}
}
Loading
Loading