ThreeMammals / Ocelot

.NET API Gateway
https://www.nuget.org/packages/Ocelot
MIT License
8.3k stars 1.63k forks source link

Routing based on Request Header #360

Closed BrettJG closed 4 months ago

BrettJG commented 6 years ago

Hi,

I'm interested if Ocelot currently supports downstream routing to multiple endpoints based on a Request Header?

A use case for this is something like:

eg. AU redirects to -> api.internal-au/products UK redirects to -> api.internal-uk/products

cheers

TomPallister commented 6 years ago

@BrettJG thanks for your interest in the project. Ocelot doesn’t support this at the moment but we could add it as a feature. I’m working on another issue at the moment but when I have finished I’ll take a look at this.

netren20000 commented 6 years ago

yes, i need it too! in A/B Testing

TomPallister commented 6 years ago

@netren20000 cool thanks for letting me know! I will try and do this ASAP! I am very busy at the moment just answering questions and fixing bugs! Hopefully once all the bugs are sorted out I will do this feature!

iderbyshev commented 6 years ago

Hi @TomPallister . First I'd like to thank you for this great product.

I'm currently trying to use it in a multi tenant solution which is hosted in Service Fabric. Based on a tenant id in a HTTP header I need to route to different instances of a particular Service Fabric application (i.e. change backend ServiceName dynamically based on current request parameters). So I implemented my custom middleware which replaces DownstreamReRoute with a dynamic one and attached it as PreQueryStringBuilderMiddleware :

    public static async Task InvokeAsync(DownstreamContext context, Func<Task> next)
    {
        var tenantId = <get it from HTTP header>;
                var reRoute = context.DownstreamReRoute;
                var tenantServiceName = $"{tenantId}.{reRoute.ServiceName}";

                context.DownstreamReRoute = new DownstreamReRoute(
                    reRoute.Key,
                    reRoute.UpstreamPathTemplate,
                    reRoute.UpstreamHeadersFindAndReplace,
                    reRoute.DownstreamHeadersFindAndReplace,
                    reRoute.DownstreamAddresses,
                    tenantServiceName,
        ...
                );
            }
        }
        await next.Invoke();
    }

Though it works like a charm, looks like a hack. So it would be great if we could use placeholders in ServiceName (and Host maybe), and these placeholders could be set from request parameters (HTTP header and/or Claim).

TomPallister commented 6 years ago

@iderbyshev nice! doesnt feel like to bad a hack! Ocelot can't do everything :) I will bear this in mind and lets see if we can do something to make this better in the future!

TomPallister commented 5 years ago

Expected Behavior / New Feature ReRoute discovery by header value in addition to path.

Actual Behavior / Motivation for New Feature I am working on versioning problem for API, that is already running in production. By the current moment version is passed with every request|response via headers.

I would like to configure Ocelot in a such way, so that v1 will be routed to Api 1, v2 to Api 2 an so on. It also seems that additional constraints on Path could be used in other scenarios.

I can think about following problem with this approach: should filtering be done by all headers, that are present in ReRoute or should it be one of the list ?

Current implementation Right now I achieve the behaviour above by adding middleware. But this seems to be a bad solution. Dear colleagues, is it possible to achieve described behaviour in any other way ?

pliolis commented 5 years ago

yes, i need it too in a multi tenant scenario.

lalocarbone commented 5 years ago

Excellent and amazing work Tom. Need it too for a multi tenant scenario as well !!!!

Phiph commented 5 years ago

Yeah header based routing would also be useful for me :) - for a multi tennant scenario

mrclayman commented 5 years ago

Hi all, I started working on this feature a few days ago since this is something we want to leverage in our deployment of Ocelot. The idea is that I want to be able to have a set of headers, each with a set of acceptable values, and be able to define whether a request should contain any or all of the defined headers to be accepted for routing to that downroute. Essentially, to say something like this in the configuration file in a downroute section:

"UpstreamHeaderRoutingOptions": {
  "Headers": {
    "Header1": [ "AllowedValue1", "AllowedValue2" ],
    "Header2": [ "AllowedValue1", "AllowedValue2" ]
  },
  "CombinationMode": "<one of Any/All>"
}

Any value from the set of values associated with a single header is currently sufficient for the header to match. Currently, my aim is to support choosing of a downroute based on a pre-configured set of headers, because that is the itch I need to scratch. I am thinking about making it possible to use placeholders in downstream path templates, but I am a little time-constrained and I have not yet researched how to achieve that. @TomPallister or anybody from the maintainer team, can I get in touch somehow? I may have a question or two. :blush:

Thanks!

jrsanbornjr commented 4 years ago

I'm looking for this functionality also. Described spot on earlier " v1 will be routed to Api 1, v2 to Api 2". Any recent news on how this effort is going?? Thanks

mrclayman commented 4 years ago

Hi @jrsanbornjr , I created a pull request #964 a few months ago for the guys to have a look at what I had done. I haven't received any feedback so I do not know if they had a look at it in the meantime.

However, I have gotten to the point where I would prefer being able to route based on claim values so I will likely look into that. Therefore, it might be preferable if my pull request was not actually merged before I get this done.

I am also strongly considering adding support for placeholders as per one of the suggestions above, so that one could specify something like this in the configuration

"UpstreamRoutingHeaders": {
  "Headers": {
    "MyHeader": [ "HeaderValue" ]
  }
},
"DownstreamPathTemplate" : "https://service/{header:MyHeader}/path"

which seems pretty nifty to me. Similarly, claims might have something like http://service/{claim:ClaimName}/path. I have not got around to this yet, though. :slightly_frowning_face:

sachinjagdale commented 4 years ago

I am also waiting for this functionality to support my legacy v1 api and v2 microservice. Will need to have unique gateway route which only be differentiated on header.

jrsanbornjr commented 4 years ago

Claims could also be good. I was looking to differentiate V1 / V2 for a service by a header value, but I think claims could do the same thing. my V1 service requires no authentication or authorization, but I'd like to add a bearer token requirement to V2 to restrict / log client activity.

On Mon, Sep 23, 2019 at 10:04 AM mrclayman notifications@github.com wrote:

Hi @jrsanbornjr https://github.com/jrsanbornjr , I created a pull request #964 https://github.com/ThreeMammals/Ocelot/pull/964 a few months ago for the guys to have a look at what I had done. I haven't received any feedback so I do not know if they had a look at it in the meantime.

However, I have gotten to the point where I would prefer being able to route based on claim values so I will likely look into that. Therefore, it might be preferable if my pull request was not actually merged before I get this done.

I am also strongly considering adding support for placeholders as per one of the suggestions above, so that one could specify something like this in the configuration

"UpstreamRoutingHeaders": {

"Headers": {

"MyHeader": [ "HeaderValue" ]

}

}, "DownstreamPathTemplate" : "https://service/{header:MyHeader}/path"

which seems pretty nifty to me. Similarly, claims might have something like http://service/{claim:ClaimName}/path. I have not got around to this yet, though. 🙁

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ThreeMammals/Ocelot/issues/360?email_source=notifications&email_token=ABW723OQHNMIOKDTSEYIG53QLDLGTA5CNFSM4FACFRPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7LFKCA#issuecomment-534140168, or mute the thread https://github.com/notifications/unsubscribe-auth/ABW723NQ64HNO4TK5XVP5QLQLDLGTANCNFSM4FACFRPA .

sachinjagdale commented 4 years ago

Thanks @jrsanbornjr for suggestion. Seems good option if routes differentiated on claims as well. Either header or claims feature support needed to differentiate v1 or v2 routes . Will also check on how to extend ocelot in my code for forwarding requesr to correct downstream routes based on header/claims. Please suggest if you have done something like that.

mrclayman commented 4 years ago

@sachinjagdale, are you going to try to implement this? I think I will finally be able to find the time over the next several days to look into adding claim-based routing and placeholder support for claims and headers, so that we don't end up messing with the same code.

SebastianMajewski commented 4 years ago

@mrclayman great job. Are you planning add header value matching? I need it for my scenario. Something like: "UpstreamRoutingHeaders": { "Headers": { "MyHeader": [ "value-{everything}" ] } }

jrsanbornjr commented 4 years ago

Apologies! I've been sidetracked lately. I will have a look at this today.

On Mon, Sep 23, 2019 at 10:04 AM mrclayman notifications@github.com wrote:

Hi @jrsanbornjr https://github.com/jrsanbornjr , I created a pull request #964 https://github.com/ThreeMammals/Ocelot/pull/964 a few months ago for the guys to have a look at what I had done. I haven't received any feedback so I do not know if they had a look at it in the meantime.

However, I have gotten to the point where I would prefer being able to route based on claim values so I will likely look into that. Therefore, it might be preferable if my pull request was not actually merged before I get this done.

I am also strongly considering adding support for placeholders as per one of the suggestions above, so that one could specify something like this in the configuration

"UpstreamRoutingHeaders": {

"Headers": {

"MyHeader": [ "HeaderValue" ]

}

}, "DownstreamPathTemplate" : "https://service/{header:MyHeader}/path"

which seems pretty nifty to me. Similarly, claims might have something like http://service/{claim:ClaimName}/path. I have not got around to this yet, though. 🙁

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ThreeMammals/Ocelot/issues/360?email_source=notifications&email_token=ABW723OQHNMIOKDTSEYIG53QLDLGTA5CNFSM4FACFRPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7LFKCA#issuecomment-534140168, or mute the thread https://github.com/notifications/unsubscribe-auth/ABW723NQ64HNO4TK5XVP5QLQLDLGTANCNFSM4FACFRPA .

mrclayman commented 4 years ago

I also apologize for the delay, other more important tasks got in the way. I managed to clear my backlog a little, so hopefully I will finally have more time now.

@SebastianMajewski it was not planned intially, but I will give some time to research how difficult it would be to implement. :slightly_smiling_face:

falcopr commented 4 years ago

Is there some update about this feature? It would be very beneficial to have this feature since depending on http header data the response from the server will change, like authentication. I saw that there was a closed Pull-Request about this: https://github.com/ThreeMammals/Ocelot/pull/964/files But somehow the progress got stuck.

brettwinters commented 4 years ago

I also think I need this feature for my multi-tenant application. I'm using different subdomains for my clients and basically I want to use the "origin" header to switch authentication schemes

route uses Finbuckle to configure via http call to a tenant service


{
        "AuthenticationOptions": {
            "AuthenticationProviderKey": "TenantAuthentication",
            "AllowedScopes": []
        }
},

and this route for admins

{
        "UpstreamRoutingHeaders": {
              "Headers": {
                     "Origin": [ "admin.xxx.com" ]
              }
        },
        "AuthenticationOptions": {
            "AuthenticationProviderKey": "AdminAuthentication",
            "AllowedScopes": []
        }
}

Unless anyone has a better suggestion. Plus I can also think of many other uses for header routing...

jrsanbornjr commented 4 years ago

My apologies! I've been buried in other projects in recent months and haven't had a chance to get back to this issue. But it is still important! I will try my best to dig back into this very soon. Thanks again!

On Mon, Sep 23, 2019 at 10:04 AM mrclayman notifications@github.com wrote:

Hi @jrsanbornjr https://github.com/jrsanbornjr , I created a pull request #964 https://github.com/ThreeMammals/Ocelot/pull/964 a few months ago for the guys to have a look at what I had done. I haven't received any feedback so I do not know if they had a look at it in the meantime.

However, I have gotten to the point where I would prefer being able to route based on claim values so I will likely look into that. Therefore, it might be preferable if my pull request was not actually merged before I get this done.

I am also strongly considering adding support for placeholders as per one of the suggestions above, so that one could specify something like this in the configuration

"UpstreamRoutingHeaders": {

"Headers": {

"MyHeader": [ "HeaderValue" ]

}

}, "DownstreamPathTemplate" : "https://service/{header:MyHeader}/path"

which seems pretty nifty to me. Similarly, claims might have something like http://service/{claim:ClaimName}/path. I have not got around to this yet, though. 🙁

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ThreeMammals/Ocelot/issues/360?email_source=notifications&email_token=ABW723OQHNMIOKDTSEYIG53QLDLGTA5CNFSM4FACFRPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7LFKCA#issuecomment-534140168, or mute the thread https://github.com/notifications/unsubscribe-auth/ABW723NQ64HNO4TK5XVP5QLQLDLGTANCNFSM4FACFRPA .

brettwinters commented 4 years ago

I'm not sure of the implications, but maybe a better option might be a "delegate route strategy" which I think is the idea here #1236 - I'm thinking of the Delegate Strategy as used here

In that way your users could use any type of logic to return a delegate value which then can be used in the routing config to determine the route

.AddOcelot()
    .WithDelegateRouting(ctx => {
        //any kind of logic here to return a string
         ((HttpContext)ctx).Request.Headers.TryGetValue("origin", out StringValues originValue);
         return Task.FromResult(originValue.ToString());
    })

then in routing config:

{
        "DelegateRouting": {
              "Values": {
                     "admin.xxx.com",
                     "another_route"
              }
        },
}
mrclayman commented 4 years ago

I owe you guys an apology, too. :disappointed: I was the creator of PR #964 back when we (at the company I work for) thought we would make use of that feature. However, we have since made the decision to go a different route that would not require this change and I just have not had the time to finish this so I had to essentially abandon all work done on the task.

I have been hoping to return to it asap, but it has not worked out that way yet.

RickJNash commented 1 year ago

@iderbyshev commented on Aug 1, 2018

Did you get this working I have the same issue

RickJNash commented 1 year ago

@mrclayman commented on Sep 23, 2019

Did you implement this feature in the main/master branch?

I have similar problem with Keycloak where I want reroute based on customers in different realms?

PraveenValavan commented 1 year ago

@RickJNash did you get it fixed for you?

raman-m commented 1 year ago

@mrclayman commented on Jun 10, 2020

Hi Zbyněk!

It is 2023 year nowadays! It is time to return back! 😉

So, I've assigned this issue to you. 😸

raman-m commented 1 year ago

@BrettJG commented on May 16, 2018

Hi Brett! Are you still interested in this feature development? I would like to discuss your user case... Your feature request seems to me a little bit strange and unclear...

First,

Question: "What cloud provider do you use for gateway app hosting and all downstream services?" If Azure, I could give you a couple of advices. Read these articles please:

Second,

If you already know the zone (DNS host name) you would like to redirect the request too, this means you get a decision or response from a service which said to you: "Redirect to this DNS host". In this case why would you like to delegate the redirection to Ocelot gateway? It seems you need just to integrate to some Service Discovery provider. And it seems your user case is closely related to Dynamic Routing feature. Do you have an intention to try these Ocelot nice features which should allow you to build multi-tenant downstream services based on some user IP location service? The rest is enabling Headers Transformation feature, and apply X-Forwarded-For example to allow forwarding of client's IP to a downstream service which has multi-tenancy deployment. So, downstream service will redirect the request to appropriate zone (DNS host) based on client's IP being forwarded by Ocelot gateway. Bingo!

Sure, you could try native Azure services for multi-tenant apps. And in this case you don't need this requested feature at all!

BrettJG commented 1 year ago

hey @raman-m , I'm sorry I don't recall exactually how we planned to use this as a feature, it was a while ago. However, we moved to an API per region architecture, so this feature is no longer required by us.
Thanks for following up.

RickJNash commented 1 year ago

@RickJNash did you get it fixed for you?

No We decided not to do this in the end

PraveenValavan commented 1 year ago

hey @raman-m We are trying to do something similar,

I have an Ocelot gateway that we are using to route our services. I also have Azure storage accounts for each Tenant and we want to call the storage apis directly from the gateway.

I used the PreQueryStringBuilderMiddleware to replace the DownstreamHostAndPort with the below code

 List<DownstreamHostAndPort> downstreamHostAndPorts = new List<DownstreamHostAndPort>()
                      {
                          new DownstreamHostAndPort($"{tenantId}{_storageAccountOptions.StorageAccountSuffix}",_storageAccountOptions.StorageAccountDefaultPort)
                      };
      httpContext.Items.UpsertDownstreamRoute(
                      new DownstreamRoute(
                          key: downstreamRoute.Key,
                          upstreamPathTemplate: downstreamRoute.UpstreamPathTemplate,
                          upstreamHeadersFindAndReplace: downstreamRoute.UpstreamHeadersFindAndReplace,
                          downstreamHeadersFindAndReplace: downstreamRoute.DownstreamHeadersFindAndReplace,
                          downstreamAddresses: downstreamHostAndPorts,
                          serviceName: downstreamRoute.ServiceName,
                          serviceNamespace: downstreamRoute.ServiceNamespace,
                          httpHandlerOptions: downstreamRoute.HttpHandlerOptions,
                          useServiceDiscovery: downstreamRoute.UseServiceDiscovery,
                          enableEndpointEndpointRateLimiting: downstreamRoute.EnableEndpointEndpointRateLimiting,
                          qosOptions: downstreamRoute.QosOptions,
                          downstreamScheme: downstreamRoute.DownstreamScheme,
                          requestIdKey: downstreamRoute.RequestIdKey,
                          isCached: downstreamRoute.IsCached,
                          cacheOptions: downstreamRoute.CacheOptions,
                          loadBalancerOptions: downstreamRoute.LoadBalancerOptions,
                          rateLimitOptions: downstreamRoute.RateLimitOptions,
                          routeClaimsRequirement: downstreamRoute.RouteClaimsRequirement,
                          claimsToQueries: downstreamRoute.ClaimsToQueries,
                          claimsToHeaders: downstreamRoute.ClaimsToHeaders,
                          claimsToClaims: downstreamRoute.ClaimsToClaims,
                          claimsToPath: downstreamRoute.ClaimsToPath,
                          isAuthenticated: downstreamRoute.IsAuthenticated,
                          isAuthorized: downstreamRoute.IsAuthorized,
                          authenticationOptions: downstreamRoute.AuthenticationOptions,
                          downstreamPathTemplate: downstreamRoute.DownstreamPathTemplate,
                          loadBalancerKey: downstreamRoute.LoadBalancerKey,
                          delegatingHandlers: downstreamRoute.DelegatingHandlers,
                          addHeadersToDownstream: downstreamRoute.AddHeadersToDownstream,
                          addHeadersToUpstream: downstreamRoute.AddHeadersToUpstream,
                          dangerousAcceptAnyServerCertificateValidator: downstreamRoute.DangerousAcceptAnyServerCertificateValidator,
                          securityOptions: downstreamRoute.SecurityOptions,
                          downstreamHttpMethod: downstreamRoute.DownstreamHttpMethod,
                          downstreamHttpVersion: downstreamRoute.DownstreamHttpVersion
                  ));
              }
              await next.Invoke();

However, this works for the first time, and the following requests don't get changed to the right host but use the host from the first request instead. This is because the LoadBalancingMiddleware kicks in and replaces the host that I have updated. Can you please guide me on the right track?

mrclayman commented 1 year ago

@mrclayman commented on Jun 10, 2020

Hi Zbyněk!

It is 2023 year nowadays! It is time to return back! 😉

So, I've assigned this issue to you. 😸

I don't know... I mean... it's been 4 years. :laughing: I don't even work for the company where we needed that. But I guess, since I don't like walking away from unfinished work, I should find the time to look into it and complete it somehow, especially now that I don't have much else to do in my spare time. :thinking:

raman-m commented 1 year ago

@PraveenValavan commented on June 29, 2023

However, this works for the first time, and the following requests don't get changed to the right host but use the host from the first request instead. This is because the LoadBalancingMiddleware kicks in and replaces the host that I have updated. Can you please guide me on the right track?

Hi Praveen! It looks like a developer's coding life hack! Nice! 🤣 Cannot suggest anything with your life hack...

I recommend going these two ways:

Hope it helps!

P.S. Sure, you can wait for final delivery of current feature in a month or two. Now we have a draft solution ( #964 ) from Zbyněk Novotný.

raman-m commented 1 year ago

@mrclayman commented on Jun 29, 2023

I don't even work for the company where we needed that.

No worries! Every company in the world could use Ocelot to build API gateways, including your current one. 😃

Regarding #964 ... It is based on very old, outdated feature branch! I've created this PR 1 to update your fork repo. Please merge the PR! After that create new feature branch from develop, apply all changes and create new PR please!

Hope you will start development soon!

raman-m commented 1 year ago

@mrclayman We have #1312 by @jlukawska @jlukawska We have #964 by @mrclayman

Who will win? :star: :yum: Your bets, please! :spades: :hearts: :clubs: :diamonds:

mrclayman commented 1 year ago

I bet on her. :smile:

I've got the changes I made in the past incorporated into the new state of the code base. I could create a new preliminary PR just so ya'll can see, but I have not tested them (unit tests do pass, though).

I would also like to allow the use of placeholders, but I will have to research how to do that first.

mrclayman commented 1 year ago

Hm, for some reason, many of the changed files are coming up as "rewritten" even though I really only just added the bits I had implemented in the old branch, dammit.

Let me know if that is a problem. :confused: Right now I feel like redoing everything. Those diffs are pretty much useless.

raman-m commented 1 year ago

@mrclayman Let's move discussion to your PR #964 until the moment you'll create a new PR, maybe...

You are unassigned from this ticket because your solution is draft and it is based on very old source code and feature branch which is 247 commits behind ThreeMammals:develop! We cannot go with your draft solution. Sorry!

This issue will be assigned to @jlukawska because of ready and solid PR #1312. The merge conflicts will be resolved soon.

mrclayman commented 1 year ago

Well, if you have another solution that is close to being merged, then it makes little sense to me to keep working on this any longer. I am perfectly fine with somebody else picking up the mantle and I don't really want this to become a race. :slightly_smiling_face:

raman-m commented 9 months ago

Possible delivery in December...

raman-m commented 4 months ago

Implemented by #1312 Will be released as a part of version 24.0...