mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-22 10:12:51 +08:00
Now defaults to case insensitive routing but you can override with a setting, also global request id setting available
This commit is contained in:
parent
30c668bfdf
commit
ff5776613f
31
README.md
31
README.md
@ -45,6 +45,18 @@ All versions can be found [here](https://www.nuget.org/packages/Ocelot/)
|
|||||||
An example configuration can be found [here](https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/configuration.json)
|
An example configuration can be found [here](https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/configuration.json)
|
||||||
and an explained configuration can be found [here](https://github.com/TomPallister/Ocelot/blob/develop/configuration-explanation.txt). More detailed instructions to come on how to configure this.
|
and an explained configuration can be found [here](https://github.com/TomPallister/Ocelot/blob/develop/configuration-explanation.txt). More detailed instructions to come on how to configure this.
|
||||||
|
|
||||||
|
There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration.
|
||||||
|
The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global
|
||||||
|
configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful
|
||||||
|
if you don't want to manage lots of ReRoute specific settings.
|
||||||
|
|
||||||
|
{
|
||||||
|
"ReRoutes": [],
|
||||||
|
"GlobalConfiguration": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
More information on how to use these options is below..
|
||||||
|
|
||||||
## Startup
|
## Startup
|
||||||
|
|
||||||
An example startup using a json file for configuration can be seen below.
|
An example startup using a json file for configuration can be seen below.
|
||||||
@ -125,8 +137,14 @@ The placeholder needs to be in both the DownstreamTemplate and UpstreamTemplate.
|
|||||||
Ocelot will attempt to replace the placeholder with the correct variable value from the
|
Ocelot will attempt to replace the placeholder with the correct variable value from the
|
||||||
Upstream URL when the request comes in.
|
Upstream URL when the request comes in.
|
||||||
|
|
||||||
At the moment all Ocelot routing is case sensitive. I think I will turn this off by default
|
At the moment without any configuration Ocelot will default to all ReRoutes being case insensitive.
|
||||||
in the future with an options to make Ocelot case sensitive per ReRoute.
|
In order to change this you can specify on a per ReRoute basis the following setting.
|
||||||
|
|
||||||
|
"ReRouteIsCaseSensitive": true
|
||||||
|
|
||||||
|
This means that when Ocelot tries to match the incoming upstream url with an upstream template the
|
||||||
|
evaluation will be case sensitive. This setting defaults to false so only set it if you want
|
||||||
|
the ReRoute to be case sensitive is my advice!
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
@ -263,6 +281,13 @@ In order to use the requestid feature in your ReRoute configuration add this set
|
|||||||
|
|
||||||
In this example OcRequestId is the request header that contains the clients request id.
|
In this example OcRequestId is the request header that contains the clients request id.
|
||||||
|
|
||||||
|
There is also a setting in the GlobalConfiguration section which will override whatever has been
|
||||||
|
set at ReRoute level for the request id. The setting is as fllows.
|
||||||
|
|
||||||
|
"RequestIdKey": "OcRequestId",
|
||||||
|
|
||||||
|
It behaves in exactly the same way as the ReRoute level RequestIdKey settings.
|
||||||
|
|
||||||
## Caching
|
## Caching
|
||||||
|
|
||||||
Ocelot supports some very rudimentary caching at the moment provider by
|
Ocelot supports some very rudimentary caching at the moment provider by
|
||||||
@ -300,6 +325,8 @@ forwarded to the downstream service. Obviously this would break everything :(
|
|||||||
and doesnt check the response is OK. I think the fact you can even call stuff
|
and doesnt check the response is OK. I think the fact you can even call stuff
|
||||||
that isnt available is annoying. Let alone it be null.
|
that isnt available is annoying. Let alone it be null.
|
||||||
|
|
||||||
|
+ The Ocelot Request Id starts getting logged too late in the pipeline.
|
||||||
|
|
||||||
## Coming up
|
## Coming up
|
||||||
|
|
||||||
You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1)
|
You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1)
|
||||||
|
@ -1,70 +1,80 @@
|
|||||||
"ReRoutes": [
|
{
|
||||||
{
|
"ReRoutes": [
|
||||||
# The url we are forwarding the request to
|
{
|
||||||
"UpstreamTemplate": "/identityserverexample",
|
# The url we are forwarding the request to
|
||||||
# The path we are listening on for this re route
|
"UpstreamTemplate": "/identityserverexample",
|
||||||
"UpstreamTemplate": "/identityserverexample",
|
# The path we are listening on for this re route
|
||||||
# The method we are listening for on this re route
|
"UpstreamTemplate": "/identityserverexample",
|
||||||
"UpstreamHttpMethod": "Get",
|
# The method we are listening for on this re route
|
||||||
# Only support identity server at the moment
|
"UpstreamHttpMethod": "Get",
|
||||||
"AuthenticationOptions": {
|
# Only support identity server at the moment
|
||||||
"Provider": "IdentityServer",
|
"AuthenticationOptions": {
|
||||||
"ProviderRootUrl": "http://localhost:52888",
|
"Provider": "IdentityServer",
|
||||||
"ScopeName": "api",
|
"ProviderRootUrl": "http://localhost:52888",
|
||||||
"AdditionalScopes": [
|
"ScopeName": "api",
|
||||||
"openid",
|
"AdditionalScopes": [
|
||||||
"offline_access"
|
"openid",
|
||||||
],
|
"offline_access"
|
||||||
# Required if using reference tokens
|
],
|
||||||
"ScopeSecret": "secret"
|
# Required if using reference tokens
|
||||||
|
"ScopeSecret": "secret"
|
||||||
|
},
|
||||||
|
# WARNING - will overwrite any headers already in the request with these values.
|
||||||
|
# Ocelot will look in the user claims for the key in [] then return the value and save
|
||||||
|
# it as a header with the given key before the colon (:). The index selection on value
|
||||||
|
# means that Ocelot will use the delimiter specified after the next > to split the
|
||||||
|
# claim value and return the index specified.
|
||||||
|
"AddHeadersToRequest": {
|
||||||
|
"CustomerId": "Claims[CustomerId] > value",
|
||||||
|
"LocationId": "Claims[LocationId] > value",
|
||||||
|
"UserType": "Claims[sub] > value[0] > |",
|
||||||
|
"UserId": "Claims[sub] > value[1] > |"
|
||||||
|
},
|
||||||
|
# WARNING - will overwrite any claims already in the request with these values.
|
||||||
|
# Ocelot will look in the user claims for the key in [] then return the value and save
|
||||||
|
# it as a claim with the given key before the colon (:). The index selection on value
|
||||||
|
# means that Ocelot will use the delimiter specified after the next > to split the
|
||||||
|
# claim value and return the index specified.
|
||||||
|
"AddClaimsToRequest": {
|
||||||
|
"CustomerId": "Claims[CustomerId] > value",
|
||||||
|
"LocationId": "Claims[LocationId] > value",
|
||||||
|
"UserType": "Claims[sub] > value[0] > |",
|
||||||
|
"UserId": "Claims[sub] > value[1] > |"
|
||||||
|
},
|
||||||
|
# WARNING - will overwrite any query string entries already in the request with these values.
|
||||||
|
# Ocelot will look in the user claims for the key in [] then return the value and save
|
||||||
|
# it as a query string with the given key before the colon (:). The index selection on value
|
||||||
|
# means that Ocelot will use the delimiter specified after the next > to split the
|
||||||
|
# claim value and return the index specified.
|
||||||
|
"AddQueriesToRequest": {
|
||||||
|
"CustomerId": "Claims[CustomerId] > value",
|
||||||
|
"LocationId": "Claims[LocationId] > value",
|
||||||
|
"UserType": "Claims[sub] > value[0] > |",
|
||||||
|
"UserId": "Claims[sub] > value[1] > |"
|
||||||
|
},
|
||||||
|
# This specifies any claims that are required for the user to access this re route.
|
||||||
|
# In this example the user must have the claim type UserType and
|
||||||
|
# the value must be registered
|
||||||
|
"RouteClaimsRequirement": {
|
||||||
|
"UserType": "registered"
|
||||||
|
},
|
||||||
|
# This tells Ocelot to look for a header and use its value as a request/correlation id.
|
||||||
|
# If it is set here then the id will be forwarded to the downstream service. If it
|
||||||
|
# does not then it will not be forwarded
|
||||||
|
"RequestIdKey": "OcRequestId",
|
||||||
|
# If this is set the response from the downstream service will be cached using the key that called it.
|
||||||
|
# This gives the user a chance to influence the key by adding some random query string paramter for
|
||||||
|
# a user id or something that would get ignored by the downstream service. This is a hack and I
|
||||||
|
# intend to provide a mechanism the user can specify for the ttl caching. Also want to expand
|
||||||
|
# the caching a lot.
|
||||||
|
"FileCacheOptions": { "TtlSeconds": 15 },
|
||||||
|
# The value of this is used when matching the upstream template to an upstream url.
|
||||||
|
"ReRouteIsCaseSensitive": false
|
||||||
},
|
},
|
||||||
# WARNING - will overwrite any headers already in the request with these values.
|
# This section is meant to be for global configuration settings
|
||||||
# Ocelot will look in the user claims for the key in [] then return the value and save
|
"GlobalConfiguration": {
|
||||||
# it as a header with the given key before the colon (:). The index selection on value
|
# If this is set it will override any route specific request id keys, behaves the same
|
||||||
# means that Ocelot will use the delimiter specified after the next > to split the
|
# otherwise
|
||||||
# claim value and return the index specified.
|
"RequestIdKey": "OcRequestId",
|
||||||
"AddHeadersToRequest": {
|
}
|
||||||
"CustomerId": "Claims[CustomerId] > value",
|
|
||||||
"LocationId": "Claims[LocationId] > value",
|
|
||||||
"UserType": "Claims[sub] > value[0] > |",
|
|
||||||
"UserId": "Claims[sub] > value[1] > |"
|
|
||||||
},
|
|
||||||
# WARNING - will overwrite any claims already in the request with these values.
|
|
||||||
# Ocelot will look in the user claims for the key in [] then return the value and save
|
|
||||||
# it as a claim with the given key before the colon (:). The index selection on value
|
|
||||||
# means that Ocelot will use the delimiter specified after the next > to split the
|
|
||||||
# claim value and return the index specified.
|
|
||||||
"AddClaimsToRequest": {
|
|
||||||
"CustomerId": "Claims[CustomerId] > value",
|
|
||||||
"LocationId": "Claims[LocationId] > value",
|
|
||||||
"UserType": "Claims[sub] > value[0] > |",
|
|
||||||
"UserId": "Claims[sub] > value[1] > |"
|
|
||||||
},
|
|
||||||
# WARNING - will overwrite any query string entries already in the request with these values.
|
|
||||||
# Ocelot will look in the user claims for the key in [] then return the value and save
|
|
||||||
# it as a query string with the given key before the colon (:). The index selection on value
|
|
||||||
# means that Ocelot will use the delimiter specified after the next > to split the
|
|
||||||
# claim value and return the index specified.
|
|
||||||
"AddQueriesToRequest": {
|
|
||||||
"CustomerId": "Claims[CustomerId] > value",
|
|
||||||
"LocationId": "Claims[LocationId] > value",
|
|
||||||
"UserType": "Claims[sub] > value[0] > |",
|
|
||||||
"UserId": "Claims[sub] > value[1] > |"
|
|
||||||
},
|
|
||||||
# This specifies any claims that are required for the user to access this re route.
|
|
||||||
# In this example the user must have the claim type UserType and
|
|
||||||
# the value must be registered
|
|
||||||
"RouteClaimsRequirement": {
|
|
||||||
"UserType": "registered"
|
|
||||||
},
|
|
||||||
# This tells Ocelot to look for a header and use its value as a request/correlation id.
|
|
||||||
# If it is set here then the id will be forwarded to the downstream service. If it
|
|
||||||
# does not then it will not be forwarded
|
|
||||||
"RequestIdKey": "OcRequestId",
|
|
||||||
# If this is set the response from the downstream service will be cached using the key that called it.
|
|
||||||
# This gives the user a chance to influence the key by adding some random query string paramter for
|
|
||||||
# a user id or something that would get ignored by the downstream service. This is a hack and I
|
|
||||||
# intend to provide a mechanism the user can specify for the ttl caching. Also want to expand
|
|
||||||
# the caching a lot.
|
|
||||||
"FileCacheOptions": { "TtlSeconds": 15 }
|
|
||||||
}
|
}
|
@ -19,6 +19,8 @@ namespace Ocelot.Configuration.Creator
|
|||||||
private readonly IConfigurationValidator _configurationValidator;
|
private readonly IConfigurationValidator _configurationValidator;
|
||||||
private const string RegExMatchEverything = ".*";
|
private const string RegExMatchEverything = ".*";
|
||||||
private const string RegExMatchEndString = "$";
|
private const string RegExMatchEndString = "$";
|
||||||
|
private const string RegExIgnoreCase = "(?i)";
|
||||||
|
|
||||||
private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser;
|
private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser;
|
||||||
private readonly ILogger<FileOcelotConfigurationCreator> _logger;
|
private readonly ILogger<FileOcelotConfigurationCreator> _logger;
|
||||||
|
|
||||||
@ -65,14 +67,56 @@ namespace Ocelot.Configuration.Creator
|
|||||||
|
|
||||||
foreach (var reRoute in _options.Value.ReRoutes)
|
foreach (var reRoute in _options.Value.ReRoutes)
|
||||||
{
|
{
|
||||||
var ocelotReRoute = SetUpReRoute(reRoute);
|
var ocelotReRoute = SetUpReRoute(reRoute, _options.Value.GlobalConfiguration);
|
||||||
reRoutes.Add(ocelotReRoute);
|
reRoutes.Add(ocelotReRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OcelotConfiguration(reRoutes);
|
return new OcelotConfiguration(reRoutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReRoute SetUpReRoute(FileReRoute reRoute)
|
private ReRoute SetUpReRoute(FileReRoute reRoute, FileGlobalConfiguration globalConfiguration)
|
||||||
|
{
|
||||||
|
var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey);
|
||||||
|
|
||||||
|
var upstreamTemplate = BuildUpstreamTemplate(reRoute);
|
||||||
|
|
||||||
|
var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions?.Provider);
|
||||||
|
|
||||||
|
var isAuthorised = reRoute.RouteClaimsRequirement?.Count > 0;
|
||||||
|
|
||||||
|
var isCached = reRoute.FileCacheOptions.TtlSeconds > 0;
|
||||||
|
|
||||||
|
var requestIdKey = globalRequestIdConfiguration
|
||||||
|
? globalConfiguration.RequestIdKey
|
||||||
|
: reRoute.RequestIdKey;
|
||||||
|
|
||||||
|
|
||||||
|
if (isAuthenticated)
|
||||||
|
{
|
||||||
|
var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider,
|
||||||
|
reRoute.AuthenticationOptions.ProviderRootUrl, reRoute.AuthenticationOptions.ScopeName,
|
||||||
|
reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes,
|
||||||
|
reRoute.AuthenticationOptions.ScopeSecret);
|
||||||
|
|
||||||
|
var claimsToHeaders = GetAddThingsToRequest(reRoute.AddHeadersToRequest);
|
||||||
|
var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest);
|
||||||
|
var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest);
|
||||||
|
|
||||||
|
return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
|
||||||
|
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
|
||||||
|
authOptionsForRoute, claimsToHeaders, claimsToClaims,
|
||||||
|
reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries,
|
||||||
|
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
|
||||||
|
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
|
||||||
|
null, new List<ClaimToThing>(), new List<ClaimToThing>(),
|
||||||
|
reRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(),
|
||||||
|
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildUpstreamTemplate(FileReRoute reRoute)
|
||||||
{
|
{
|
||||||
var upstreamTemplate = reRoute.UpstreamTemplate;
|
var upstreamTemplate = reRoute.UpstreamTemplate;
|
||||||
|
|
||||||
@ -94,38 +138,9 @@ namespace Ocelot.Configuration.Creator
|
|||||||
upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything);
|
upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything);
|
||||||
}
|
}
|
||||||
|
|
||||||
upstreamTemplate = $"{upstreamTemplate}{RegExMatchEndString}";
|
return reRoute.ReRouteIsCaseSensitive
|
||||||
|
? $"{upstreamTemplate}{RegExMatchEndString}"
|
||||||
var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions?.Provider);
|
: $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}";
|
||||||
|
|
||||||
var isAuthorised = reRoute.RouteClaimsRequirement?.Count > 0;
|
|
||||||
|
|
||||||
var isCached = reRoute.FileCacheOptions.TtlSeconds > 0;
|
|
||||||
|
|
||||||
if (isAuthenticated)
|
|
||||||
{
|
|
||||||
var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider,
|
|
||||||
reRoute.AuthenticationOptions.ProviderRootUrl, reRoute.AuthenticationOptions.ScopeName,
|
|
||||||
reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes,
|
|
||||||
reRoute.AuthenticationOptions.ScopeSecret);
|
|
||||||
|
|
||||||
var claimsToHeaders = GetAddThingsToRequest(reRoute.AddHeadersToRequest);
|
|
||||||
var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest);
|
|
||||||
var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest);
|
|
||||||
|
|
||||||
return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
|
|
||||||
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
|
|
||||||
authOptionsForRoute, claimsToHeaders, claimsToClaims,
|
|
||||||
reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries,
|
|
||||||
reRoute.RequestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
|
|
||||||
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
|
|
||||||
null, new List<ClaimToThing>(), new List<ClaimToThing>(),
|
|
||||||
reRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(),
|
|
||||||
reRoute.RequestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ClaimToThing> GetAddThingsToRequest(Dictionary<string,string> thingBeingAdded)
|
private List<ClaimToThing> GetAddThingsToRequest(Dictionary<string,string> thingBeingAdded)
|
||||||
|
@ -7,8 +7,10 @@ namespace Ocelot.Configuration.File
|
|||||||
public FileConfiguration()
|
public FileConfiguration()
|
||||||
{
|
{
|
||||||
ReRoutes = new List<FileReRoute>();
|
ReRoutes = new List<FileReRoute>();
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FileReRoute> ReRoutes { get; set; }
|
public List<FileReRoute> ReRoutes { get; set; }
|
||||||
|
public FileGlobalConfiguration GlobalConfiguration { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
src/Ocelot/Configuration/File/FileGlobalConfiguration.cs
Normal file
7
src/Ocelot/Configuration/File/FileGlobalConfiguration.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Ocelot.Configuration.File
|
||||||
|
{
|
||||||
|
public class FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
public string RequestIdKey { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -24,5 +24,6 @@ namespace Ocelot.Configuration.File
|
|||||||
public Dictionary<string, string> AddQueriesToRequest { get; set; }
|
public Dictionary<string, string> AddQueriesToRequest { get; set; }
|
||||||
public string RequestIdKey { get; set; }
|
public string RequestIdKey { get; set; }
|
||||||
public FileCacheOptions FileCacheOptions { get; set; }
|
public FileCacheOptions FileCacheOptions { get; set; }
|
||||||
|
public bool ReRouteIsCaseSensitive { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
200
test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs
Normal file
200
test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Ocelot.Configuration.File;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
public class CaseSensitiveRoutingTests : IDisposable
|
||||||
|
{
|
||||||
|
private IWebHost _builder;
|
||||||
|
private readonly Steps _steps;
|
||||||
|
|
||||||
|
public CaseSensitiveRoutingTests()
|
||||||
|
{
|
||||||
|
_steps = new Steps();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_when_global_ignore_case_sensitivity_set()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:51879/api/products/{productId}",
|
||||||
|
UpstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_when_reroute_ignore_case_sensitivity_set()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:51879/api/products/{productId}",
|
||||||
|
UpstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_404_when_reroute_respect_case_sensitivity_set()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:51879/api/products/{productId}",
|
||||||
|
UpstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_when_reroute_respect_case_sensitivity_set()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:51879/api/products/{productId}",
|
||||||
|
UpstreamTemplate = "/PRODUCTS/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_404_when_global_respect_case_sensitivity_set()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:51879/api/products/{productId}",
|
||||||
|
UpstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_when_global_respect_case_sensitivity_set()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:51879/api/products/{productId}",
|
||||||
|
UpstreamTemplate = "/PRODUCTS/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1"))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody)
|
||||||
|
{
|
||||||
|
_builder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_builder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_builder?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -76,6 +76,36 @@ namespace Ocelot.AcceptanceTests
|
|||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_use_global_request_id_and_forward()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:51879/",
|
||||||
|
UpstreamTemplate = "/",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
RequestIdKey = _steps.RequestIdKey
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/", requestId))
|
||||||
|
.Then(x => _steps.ThenTheRequestIdIsReturned(requestId))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
private void GivenThereIsAServiceRunningOn(string url)
|
private void GivenThereIsAServiceRunningOn(string url)
|
||||||
{
|
{
|
||||||
_builder = new WebHostBuilder()
|
_builder = new WebHostBuilder()
|
||||||
|
@ -1 +1 @@
|
|||||||
{"ReRoutes":[{"DownstreamTemplate":"http://localhost:41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0}}]}
|
{"ReRoutes":[{"DownstreamTemplate":"http://localhost:41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false}],"GlobalConfiguration":{"RequestIdKey":null}}
|
@ -36,7 +36,37 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void should_create_template_pattern_that_matches_anything_to_end_of_string()
|
public void should_use_reroute_case_sensitivity_value()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheConfigIs(new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
UpstreamTemplate = "/api/products/{productId}",
|
||||||
|
DownstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.And(x => x.GivenTheConfigIsValid())
|
||||||
|
.When(x => x.WhenICreateTheConfig())
|
||||||
|
.Then(x => x.ThenTheReRoutesAre(new List<ReRoute>
|
||||||
|
{
|
||||||
|
new ReRouteBuilder()
|
||||||
|
.WithDownstreamTemplate("/products/{productId}")
|
||||||
|
.WithUpstreamTemplate("/api/products/{productId}")
|
||||||
|
.WithUpstreamHttpMethod("Get")
|
||||||
|
.WithUpstreamTemplatePattern("(?i)/api/products/.*$")
|
||||||
|
.Build()
|
||||||
|
}))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_upstream_template_pattern_to_ignore_case_sensitivity()
|
||||||
{
|
{
|
||||||
this.Given(x => x.GivenTheConfigIs(new FileConfiguration
|
this.Given(x => x.GivenTheConfigIs(new FileConfiguration
|
||||||
{
|
{
|
||||||
@ -49,6 +79,101 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
UpstreamHttpMethod = "Get"
|
UpstreamHttpMethod = "Get"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}))
|
||||||
|
.And(x => x.GivenTheConfigIsValid())
|
||||||
|
.When(x => x.WhenICreateTheConfig())
|
||||||
|
.Then(x => x.ThenTheReRoutesAre(new List<ReRoute>
|
||||||
|
{
|
||||||
|
new ReRouteBuilder()
|
||||||
|
.WithDownstreamTemplate("/products/{productId}")
|
||||||
|
.WithUpstreamTemplate("/api/products/{productId}")
|
||||||
|
.WithUpstreamHttpMethod("Get")
|
||||||
|
.WithUpstreamTemplatePattern("(?i)/api/products/.*$")
|
||||||
|
.Build()
|
||||||
|
}))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_upstream_template_pattern_to_respect_case_sensitivity()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheConfigIs(new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
UpstreamTemplate = "/api/products/{productId}",
|
||||||
|
DownstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.And(x => x.GivenTheConfigIsValid())
|
||||||
|
.When(x => x.WhenICreateTheConfig())
|
||||||
|
.Then(x => x.ThenTheReRoutesAre(new List<ReRoute>
|
||||||
|
{
|
||||||
|
new ReRouteBuilder()
|
||||||
|
.WithDownstreamTemplate("/products/{productId}")
|
||||||
|
.WithUpstreamTemplate("/api/products/{productId}")
|
||||||
|
.WithUpstreamHttpMethod("Get")
|
||||||
|
.WithUpstreamTemplatePattern("/api/products/.*$")
|
||||||
|
.Build()
|
||||||
|
}))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_set_global_request_id_key()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheConfigIs(new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
UpstreamTemplate = "/api/products/{productId}",
|
||||||
|
DownstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration
|
||||||
|
{
|
||||||
|
RequestIdKey = "blahhhh"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.And(x => x.GivenTheConfigIsValid())
|
||||||
|
.When(x => x.WhenICreateTheConfig())
|
||||||
|
.Then(x => x.ThenTheReRoutesAre(new List<ReRoute>
|
||||||
|
{
|
||||||
|
new ReRouteBuilder()
|
||||||
|
.WithDownstreamTemplate("/products/{productId}")
|
||||||
|
.WithUpstreamTemplate("/api/products/{productId}")
|
||||||
|
.WithUpstreamHttpMethod("Get")
|
||||||
|
.WithUpstreamTemplatePattern("/api/products/.*$")
|
||||||
|
.WithRequestIdKey("blahhhh")
|
||||||
|
.Build()
|
||||||
|
}))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_create_template_pattern_that_matches_anything_to_end_of_string()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheConfigIs(new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
UpstreamTemplate = "/api/products/{productId}",
|
||||||
|
DownstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
.And(x => x.GivenTheConfigIsValid())
|
.And(x => x.GivenTheConfigIsValid())
|
||||||
.When(x => x.WhenICreateTheConfig())
|
.When(x => x.WhenICreateTheConfig())
|
||||||
@ -95,6 +220,7 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
UpstreamTemplate = "/api/products/{productId}",
|
UpstreamTemplate = "/api/products/{productId}",
|
||||||
DownstreamTemplate = "/products/{productId}",
|
DownstreamTemplate = "/products/{productId}",
|
||||||
UpstreamHttpMethod = "Get",
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true,
|
||||||
AuthenticationOptions = new FileAuthenticationOptions
|
AuthenticationOptions = new FileAuthenticationOptions
|
||||||
{
|
{
|
||||||
AdditionalScopes = new List<string>(),
|
AdditionalScopes = new List<string>(),
|
||||||
@ -153,6 +279,7 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
UpstreamTemplate = "/api/products/{productId}",
|
UpstreamTemplate = "/api/products/{productId}",
|
||||||
DownstreamTemplate = "/products/{productId}",
|
DownstreamTemplate = "/products/{productId}",
|
||||||
UpstreamHttpMethod = "Get",
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true,
|
||||||
AuthenticationOptions = new FileAuthenticationOptions
|
AuthenticationOptions = new FileAuthenticationOptions
|
||||||
{
|
{
|
||||||
AdditionalScopes = new List<string>(),
|
AdditionalScopes = new List<string>(),
|
||||||
@ -183,7 +310,8 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
{
|
{
|
||||||
UpstreamTemplate = "/api/products/{productId}/variants/{variantId}",
|
UpstreamTemplate = "/api/products/{productId}/variants/{variantId}",
|
||||||
DownstreamTemplate = "/products/{productId}",
|
DownstreamTemplate = "/products/{productId}",
|
||||||
UpstreamHttpMethod = "Get"
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@ -212,7 +340,8 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
{
|
{
|
||||||
UpstreamTemplate = "/api/products/{productId}/variants/{variantId}/",
|
UpstreamTemplate = "/api/products/{productId}/variants/{variantId}/",
|
||||||
DownstreamTemplate = "/products/{productId}",
|
DownstreamTemplate = "/products/{productId}",
|
||||||
UpstreamHttpMethod = "Get"
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@ -241,7 +370,8 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
{
|
{
|
||||||
UpstreamTemplate = "/",
|
UpstreamTemplate = "/",
|
||||||
DownstreamTemplate = "/api/products/",
|
DownstreamTemplate = "/api/products/",
|
||||||
UpstreamHttpMethod = "Get"
|
UpstreamHttpMethod = "Get",
|
||||||
|
ReRouteIsCaseSensitive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
@ -128,6 +128,27 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher
|
|||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_ignore_case_sensitivity()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/"))
|
||||||
|
.And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("(?i)api/product/products/.*/categories/.*/variant/$"))
|
||||||
|
.When(x => x.WhenIMatchThePaths())
|
||||||
|
.Then(x => x.ThenTheResultIsTrue())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_respect_case_sensitivity()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/"))
|
||||||
|
.And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/.*/categories/.*/variant/$"))
|
||||||
|
.When(x => x.WhenIMatchThePaths())
|
||||||
|
.Then(x => x.ThenTheResultIsFalse())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void GivenIHaveAUpstreamPath(string downstreamPath)
|
private void GivenIHaveAUpstreamPath(string downstreamPath)
|
||||||
{
|
{
|
||||||
_downstreamUrlPath = downstreamPath;
|
_downstreamUrlPath = downstreamPath;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user