RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.77k stars 1.29k forks source link

hosting Swagger UI behind reverse proxy #3192

Open Simonl9l opened 3 years ago

Simonl9l commented 3 years ago

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 commented 3 years ago

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

Simonl9l commented 3 years ago

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

Work around recommendations appreciated!

jeremyVignelles commented 3 years ago

Are you looking for this kind of code ? https://github.com/RicoSuter/NSwag/blob/b63faa8bb9f1dea7df985ea79cd3089cad3fa44c/samples/WithMiddleware/Sample.AspNetCore21.Nginx/Startup.cs#L57-L80

Simonl9l commented 3 years ago

@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 commented 3 years ago

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

Simonl9l commented 3 years ago

@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 commented 3 years ago

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

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

Simonl9l commented 3 years ago

@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 commented 6 months ago

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