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

hosting Swagger UI behind reverse proxy #3192

Open
Simonl9l opened this issue Nov 25, 2020 · 9 comments
Open

hosting Swagger UI behind reverse proxy #3192

Simonl9l opened this issue Nov 25, 2020 · 9 comments

Comments

@Simonl9l
Copy link

Hi (@jeremyVignelles - pre other thread) - Per this documentation

We're hosting behind an Envoy Mesh Gateway, in a micro services environment. We have public/internet accessing hosts and more importantly internal hosts, that also support the swagger routes such they they are not exposed externally - such that we can use them for diagnostics and testing.

Most of our micro services have a route prefix behind the reverse-proxy, with rewrite rules. we host one of the micro services on the default route without a route prefix (/), and all our calls to get to swagger from the other micro services (say /service1/swagger) are falling back to that (/swagger).

Per the Envoy logging (abbreviated) we get this sequence of calls:

"GET /service1/swagger" -> 127.0.0.1:5012 this correctly routes to the correct micro service
"GET /swagger/index.html HTTP/1.1" -> "127.0.0.1:5014" looses the route...so directs a a different micro service (Note different Port)
"GET /swagger/v1/swagger.json -> "127.0.0.1:5014" as above.

Of note that being behind Envoy, and differing from IIS/NginX, due to some other testing it's know that we see anx-forwarded-for header. Again note the lower case. Perhaps the default implementation needs to be case insensitive ?

@jeremyVignelles
Copy link
Collaborator

Did you try to override the "default" implementation to your own and see if that fixes your issue?

@Simonl9l
Copy link
Author

Simonl9l commented Nov 25, 2020

@jeremyVignelles thanks - how does one recommend going about that?

Work around recommendations appreciated!

@jeremyVignelles
Copy link
Collaborator

jeremyVignelles commented Nov 25, 2020

Are you looking for this kind of code ?

// Config with custom proxy headers
//app.UseSwagger(config =>
//{
// config.Path = "/swagger/v1/swagger.json";
// config.PostProcess = (document, request) =>
// {
// if (request.Headers.ContainsKey("X-External-Host"))
// {
// // Change document server settings to public
// document.Host = request.Headers["X-External-Host"].First();
// document.BasePath = request.Headers["X-External-Path"].First();
// }
// };
//});
//app.UseSwaggerUi3(config =>
//{
// config.SwaggerRoute = "/swagger/v1/swagger.json";
// config.TransformToExternalPath = (internalUiRoute, request) =>
// {
// // The header X-External-Path is set in the nginx.conf file
// var externalPath = request.Headers.ContainsKey("X-External-Path") ? request.Headers["X-External-Path"].First() : "";
// return externalPath + internalUiRoute;
// };
//});

@Simonl9l
Copy link
Author

@jeremyVignelles - well if you mean uncomment the commented code (and make the X-External-Path lowercase, then I guess yes...I assume I don't need the

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

parts etc.

@jeremyVignelles
Copy link
Collaborator

UseForwardedHeaders seems to be asp.net core thing. not sure what it does and whether you should keep it

@Simonl9l
Copy link
Author

Simonl9l commented Nov 26, 2020

@jeremyVignelles - yes not sure why it's in the sample?

So I have modified the code as suggested above, however from an Envoy perspective it does not use the X-External-Path header, but does make the x-envoy-original-path header available as documented here. This is the full path prior to any rewrite rules.

Given my routing per the original post (as /service1/swagger) I'd need to split the path and pull the first part to use as the external path (per the code sample), such as not to have a repeating swagger route element. I've no idea is there is a more NSwag elegant solution?

app.UseSwaggerUi3(config =>
{
    config.TransformToExternalPath = (internalUiRoute, request) =>
    {
        var externalPath = !request.Headers.ContainsKey("x-envoy-original-path") ? "" :
            request.Headers["x-envoy-original-path"].First().Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
        return externalPath + internalUiRoute;
     };
});

However this still does not work, and there must be more needed, as it ends up at the default route (/) swagger endpoint/service with the envoy logging showing the requests:

GET /service1/swagger/index.html -> "127.0.0.1:5012 <-- correct service
GET /swagger/v1/swagger.json HTTP/1.1" -> 127.0.0.1:5014 <-- incorrect (default) service also note swagger path is also incorrect
GET /service1/swagger/swagger-ui-standalone-preset.js.map -> "127.0.0.1:5012 <-- correct service
GET /service1/swagger/swagger-ui.css.map  -> 127.0.0.1:5012 <-- correct service
GET /service1/swagger/swagger-ui-bundle.js.map -> 127.0.0.1:5012 <-- correct service

Of note: something is obviously missing to have the swagger.json not be routed to the correct service, and the other files are generic just rendering the swagger.json?

Any suggestion given deeper NSwag knowledge how this might be fixed ?

@RicoSuter is thee a need to consider if the default implementation based on this documentation (and referenced here) perhaps with some configurability of both the header name used, and how the route is redefined?

To confirm using the implementation per the documentation we end up per login with these endpoints being hit

GET /service1/swagger -> 127.0.0.1:5012
GET /swagger/index.html -> 127.0.0.1:5014
GET /swagger/v1/swagger.json -> 127.0.0.1:5014
GET /swagger/swagger-ui.css.map -> 127.0.0.1:5014
GET /swagger/swagger-ui-bundle.js.map -> 127.0.0.1:5014
GET /swagger/swagger-ui-standalone-preset.js.map -> 127.0.0.1:5014

So not working at all.

All help greatly appreciated.

@jeremyVignelles
Copy link
Collaborator

Did you change the code in the document generation settings too?

I don't see the point of changing the default handling if :

  • The headers of your service are not standard
  • The default behavior can be overriden quite easily

@Simonl9l
Copy link
Author

@jeremyVignelles Thanks for the quick turn around...

I assume you mean this:

app.UseOpenApi(config => config.PostProcess = (document, request) =>
{
    if (request.Headers.ContainsKey("X-External-Host"))
    {
        // Change document server settings to public
        document.Host = request.Headers["X-External-Host"].First();
        document.BasePath = request.Headers["X-External-Path"].First();
     }
});

Just seeing how I'd make this work in my case given above.

Well I'd say that NginX/IIS is just a part of the revers proxy universe. Envoy is now part of the AWS AppMesh implementation, so it's hardly "not standard"!.

Why have users of NSwag jump though hoops decoding the documentation (and missing elements of swashbuckle). Why do the X-External-Host default implementation ?

@skironDotNet
Copy link

Swagger UI is not the problem. You need to configure whole app to be aware of being behind proxy to have proper host and scheme in http context. Read this first https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-6.0

Now sample code

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.AllowedHosts.Add("*.some.domain.com"); //this will allow from any subdoamin like appX.some.domain.com
    //you can set many and pull this directly from env like builder.Configuration.GetValue<string>("PROXY_DOMAIN")

    if (builder.Environment.IsLocalDevelopment()) //this is our extention to tell us we are at localhost via  "ASPNETCORE_ENVIRONMENT": "Local"
    {
        options.AllowedHosts.Add("localhost"); //don't want this in production
    }

    //Host is a must to handle the domain, Proto is needed to properly handle SSL termination at proxy [FD Https] -> [Origin Http]
    options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto;
});

...

var app = builder.Build();
//!! just after builder.Build();
app.UseForwardedHeaders(); //this will adjust your HTTP context host and scheme in case you use SSL termination at proxy Proxy Https -> http origin, see options.ForwardedHeaders

//app.UsePathBase(new PathString("/yourRoute"));  //this doesn't work!!!
//for a case you host https://appX.domain.com and use reverse proxy ex. Azure Front door to expose as https://appB.domain/yourRoute/ you need to add PathBase

var proxyPath = "/yourRoute"; pull this from env like builder.Configuration.GetValue<string>("PROXY_ROUTE") //or whatever other config

if (!string.IsNullOrEmpty(proxyPath))
{
  app.Use((context, next) =>
  {
    //for FD this is static, in localhost via ngnix must add this header in the ngnix conf. 
    //You can use config here any other header I call proxy marker, so the app knows is behind proxy, 
    //because you don't want to add to PathBase when loading from origin location of https://appX.domain.com
      if (context.Request.Headers.Any(x => x.Key == "X-Azure-FDID")) 
      {
          context.Request.PathBase = new PathString(proxyPath);
      }
      return next(context);
  });
}

//CONTINUE THE USUAL PIPELINE REGISTRATION
//app.UseXYZ

Having proper values in HttpContext.Request will make SwaggerUI work correctly without additional hacking. Also all kinds of OAuth redirects, etc. will relay HttpContext.Request.Host so this setup is a must for the whole app to route properly.
I hope this helps.

Sample 1
image

Sample 2
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants